From 51b0bafb801880d72a55f982f4d2001e7f58d5cb Mon Sep 17 00:00:00 2001 From: dylan <56566724+d-l-n@users.noreply.github.com> Date: Sun, 16 Aug 2020 17:32:35 -0300 Subject: [PATCH 01/72] Update strings.xml updated some translations. also i suggest to change the "blacklist" strings/references for a more inclusive name like denylist or blocklist as even Linux did. --- app/src/main/res/values-es-rES/strings.xml | 98 +++++++++++----------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index c58a3e9e0..4ae9f8692 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -51,7 +51,7 @@ Añadir letra Agregar foto "Agregar a lista de reproducción" - Añadir retardo para letras + Añadir retraso para letras "Se ha agregado 1 canción a la cola de reproducción" %1$d canciones agregadas a la cola de reproducción Álbum @@ -74,7 +74,7 @@ Automático Color base del tema Refuerzo de graves - Biografía + Bio Biografía Negro Lista Negra @@ -82,7 +82,7 @@ Tarjeta con desenfoque Enviando el reporte a GitHub... Token de acceso inválido. Por favor, contacta con el desarrollador de la aplicación - El problema no esta habilitado para el repositorio seleccionado. Por favor, contacta col el desarrollador de la app. + El problema no está habilitado para el repositorio seleccionado. Por favor, contacta con el desarrollador de la app. Se ha producido un error inesperado. Por favor, contacta con el desarrollador de la aplicación Usuario o contraseña incorrectos Problema @@ -183,7 +183,7 @@ 6 7 8 - Cuadricula y estilo + Cuadrícula y estilo Giro Historial Inicio @@ -207,10 +207,10 @@ Última canción Vamos a tocar un poco de música Biblioteca - Categorías biblioteca + Categorías de la Biblioteca Licencias Blanco claro - Escuchadores + Oyentes Listando archivos Cargando productos... Iniciar Sección @@ -230,9 +230,9 @@ No hay Álbumes No hay Artistas "Primero reproduce una canción, luego intenta de nuevo." - No se encontró ecualizador + No se encontró nigún ecualizador No hay Géneros - No se encontró letra + No se encontró la letra No hay canciones tocando No hay Listas de Reproducción No se encontraron compras. @@ -256,7 +256,7 @@ Contraseña Más de 3 meses Pegar letra aquí - Peak + Pica Permiso de acceso al almacenamiento externo denegado. Permiso denegado. Personalizar @@ -289,7 +289,7 @@ Biblioteca Pantalla de bloqueo Listas de reproducción - Pausar la reproducción cuando se esta en silencio y reproducir cuando se aumenta el volumen. ¡Cuidado! Cuando se aumenta el volumen se empezara la reproducción aunque se este fuera de la app. + Pausar la reproducción cuando se está en silencio y reproducir cuando se aumenta el volumen. ¡Cuidado! Cuando se aumenta el volumen se empezará la reproducción aunque se esté fuera de la app. Pausar en cero Tenga en cuenta que habilitar esta función puede afectar la duración de la batería Mantener la pantalla encendida @@ -306,14 +306,14 @@ El color del fondo y los botones de control cambian de acuerdo a la portada del álbum para la ventana de reproducción Colorea los accesos directos de la aplicación en el color de énfasis. Cada vez que cambie el color, active esta opción Colorea la barra de navegación con el color principal - "Colorea la notificaci\u00f3n con el color vibrante de la portada del \u00e1lbum" + "Colorea la notificación con el color vibrante de la portada del álbum" Según las líneas de la guía Material Design en los colores del modo oscuro deben ser desaturados Se tomará el color dominante de la portada del álbum o imagen del artista Añadir controles extra al mini reproductor - Mostrar información extra de canciones, como el formato de archivo, bitrate y frecuencia + Mostrar información extra de canciones, como el formato de archivo, tasa de bits y frecuencia "Puede causar problemas de reproducción en algunos dispositivos" - Mostrar/Ocultar pestaña de géneros - Mostrar/Ocultar banner en inicio + Mostrar/Ocultar pestaña Géneros + Mostrar/Ocultar banner en Inicio Puede aumentar la calidad de la portada del álbum, pero provoca tiempos de carga de imágenes más lentos. Solo habilite esto si tiene problemas con portadas de baja resolución Configure la visibilidad y el orden de las categorías de la biblioteca. Usar los controles personalizados de Retro Music en la pantalla de bloqueo @@ -321,32 +321,32 @@ Redondear las esquinas de la aplicación Mostrar/Ocultar nombres de las pestañas de navegación Modo inmersivo - Comenzar a reproducir inmediatamente se conecten audífonos + Comenzar a reproducir inmediatamente cuando se conecten audífonos El modo aleatorio se desactivará cuando se reproduzca una nueva lista de canciones Mostrar controles de volumen si hay suficiente espacio disponible. Mostrar/Ocultar portada del álbum Tema de la portada del álbum Estilo de portada del álbum en reproducción - Cuadricula del álbum + Cuadrícula del álbum Accesos directos de la aplicación coloreados - Cuadricula de los artistas - Reducir el volumen en pérdida de enfoque + Cuadrícula de los artistas + Reducir el volumen cuando se pierda el enfoque Descarga automática de imágenes de artistas Lista Negra Reproducción por Bluetooth Desenfocar portada del álbum Elegir ecualizador Diseño de notificación clásico - Color adaptativo + Color Adaptativo Notificación coloreada Color Desaturado Controles extra Información de la canción Reproducción sin pausas Tema de la aplicación - Mostrar pestaña de géneros - Cuadricula de los artistas en inicio - Banner de inicio + Mostrar pestaña Géneros + Cuadrícula de los artistas en inicio + Banner de Inicio Ignorar las portadas de la biblioteca de medios Intervalo de la lista \"Añadidos Recientemente\" Controles en pantalla completa @@ -355,7 +355,7 @@ Licencias de código abierto Bordes de las esquinas Forma de los títulos de las pestañas - Efecto carrusel + Efecto Carrusel Color dominante Aplicación en pantalla completa Títulos de las pestañas @@ -364,7 +364,7 @@ Controles de volumen Información de usuario Color principal - El color principal del tema, por defecto es gris azulado, por ahora funciona con colores oscuros + El color principal del tema, por defecto gris azulado, por ahora funciona con colores oscuros Pro Temas en reproducción, efecto Carrusel, tema de color y más ... Perfil @@ -378,7 +378,7 @@ Eliminar Eliminar foto del banner Eliminar portada - Eliminar de la lista negra + Eliminar de la Lista Negra Eliminar foto de perfil Eliminar canción de la lista %1$s de la lista?]]> @@ -393,8 +393,8 @@ Compra anterior restaurada. Por favor, reinicie la aplicación para hacer uso de todas las funciones. Compras anteriores restauradas. Restaurando compra... - Ecualizador de Reto Music - Reproductor de Música Retro + Ecualizador de Retro Music + Reproductor de Retro Music Retro Music Pro La eliminación del archivo falló @@ -471,12 +471,12 @@ Tablero ¡Buenas Tardes! ¡Buen Día! - ¡Buenas Tardes! - ¡Buenos días! - Buenas noches + ¡Buenas Noches! + ¡Buen Día! + ¡Buenas Noches! ¿Cómo te llamas? Hoy - Álbumes mas reproducidos + Álbumes más reproducidos Artistas más reproducidos "Pista (2 para pista 2 o 3004 para CD3 pista 4)" Número de pista @@ -485,7 +485,7 @@ Twitter Comparte tu diseño con Retro Music Sin etiqueta - No se pudo reproducir esta canci\u00f3n + No se pudo reproducir esta canción A continuación Actualizar imagen Actualizando... @@ -504,37 +504,37 @@ %1$d seleccionados Año Tienes que seleccionar al menos una categoría - Sera redirigido al sitio web para reportar problemas. + Será redirigido al sitio web para reportar problemas. Los datos de tu cuenta sólo se utilizan para la autenticación Cantidad Nota (Opcional) Iniciar pago Mostrar en pantalla de reproducción Al hacer clic en la notificación se mostrará la pantalla de reproducción en lugar de la pantalla de inicio - Tiny card - About %s - Select language - Translators - The people who helped translate this app - Try Retro Music Premium + Carta pequeña + Acerca de %s + Seleccionar idioma + Traductores + La gente que ayudó a traductir esta app + Prueba Retro Music Premium - Song - Songs + Canción + Canciones - Album - Albums + Álbum + Álbumes - %d Song - %d Songs + %d Canción + %d Canciones - %d Album - %d Albums + %d Álbum + %d Álbumes - %d Artist - %d Artists + %d Artista + %d Artistas From bbd1853e78011d544672a8dace7c82d55acce99a Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 13 Sep 2020 21:42:12 -0400 Subject: [PATCH 02/72] Minimum working prototype --- .../java/code/name/monkey/retromusic/Constants.kt | 1 + .../monkey/retromusic/fragments/LibraryViewModel.kt | 11 +++++++++-- .../monkey/retromusic/repository/ArtistRepository.kt | 5 ++++- .../name/monkey/retromusic/util/PreferenceUtil.kt | 9 ++++++++- app/src/main/res/values/strings.xml | 4 ++++ app/src/main/res/xml/pref_ui.xml | 7 +++++++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 4e3a7a241..f0ce5dbd1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -123,6 +123,7 @@ const val AUTO_DOWNLOAD_IMAGES_POLICY = "auto_download_images_policy" const val START_DIRECTORY = "start_directory" const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval" const val LOCK_SCREEN = "lock_screen" +const val ALBUM_ARTISTS_ONLY = "album_artists_only" const val ALBUM_DETAIL_SONG_SORT_ORDER = "album_detail_song_sort_order" const val LYRICS_OPTIONS = "lyrics_tab_position" const val CHOOSE_EQUALIZER = "choose_equalizer" diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 3999ee708..ffccd49ca 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -12,6 +12,7 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.MusicServiceEventListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch @@ -91,8 +92,14 @@ class LibraryViewModel( } private fun fetchArtists() { - viewModelScope.launch(IO) { - artists.postValue(repository.fetchArtists()) + if (PreferenceUtil.albumArtistsOnly) { + viewModelScope.launch(IO) { + artists.postValue(repository.albumArtists()) + } + } else { + viewModelScope.launch(IO) { + artists.postValue(repository.fetchArtists()) + } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt index 9a309b2aa..bf1b41e35 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt @@ -69,7 +69,10 @@ class RealArtistRepository( getSongLoaderSortOrder() ) ) - return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)) + + val sortString = if (PreferenceUtil.artistSortOrder.contains("DESC")) String.CASE_INSENSITIVE_ORDER.reversed() else String.CASE_INSENSITIVE_ORDER + + return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)).sortedWith(compareBy(sortString) { it.name }) } private fun splitIntoAlbumArtists(albums: List): List { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt index 883336841..22221639e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt @@ -122,6 +122,13 @@ object PreferenceUtil { "only_wifi" ) + var albumArtistsOnly + get() = sharedPreferences.getBoolean( + ALBUM_ARTISTS_ONLY, + false + ) + set(value) = sharedPreferences.edit { putBoolean(ALBUM_ARTISTS_ONLY, value) } + var albumDetailSongSortOrder get() = sharedPreferences.getStringOrDefault( ALBUM_DETAIL_SONG_SORT_ORDER, @@ -150,7 +157,7 @@ object PreferenceUtil { var artistSortOrder get() = sharedPreferences.getStringOrDefault( ARTIST_SORT_ORDER, - AlbumSortOrder.ALBUM_A_Z + ArtistSortOrder.ARTIST_A_Z ) set(value) = sharedPreferences.edit { putString(ARTIST_SORT_ORDER, value) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 018bdc9f8..be213e122 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ Sort order + Album artists only + Tag editor Toggle favorite @@ -517,6 +519,7 @@ Snow fall effect + Show Album Artists in the Artist category Use the currently playing song album cover as the lockscreen wallpaper Lower the volume when a system sound is played or a notification is received The content of blacklisted folders is hidden from your library. @@ -546,6 +549,7 @@ Shuffle mode will turn off when playing a new list of songs If enough space is available, show volume controls in the now playing screen + Navigate by Album Artist Show album cover Album cover theme Album cover skip diff --git a/app/src/main/res/xml/pref_ui.xml b/app/src/main/res/xml/pref_ui.xml index 364756242..50c1e7d29 100644 --- a/app/src/main/res/xml/pref_ui.xml +++ b/app/src/main/res/xml/pref_ui.xml @@ -45,6 +45,13 @@ android:negativeButtonText="@null" android:positiveButtonText="@null" android:title="@string/pref_title_tab_text_mode" /> + + Date: Mon, 14 Sep 2020 01:13:15 -0400 Subject: [PATCH 03/72] Album art fallback working --- .../retromusic/glide/ArtistGlideRequest.java | 2 +- .../glide/artistimage/ArtistImageLoader.kt | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java index 4d9d2124b..c11ffcc2d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java @@ -62,7 +62,7 @@ public class ArtistGlideRequest { boolean hasCustomImage = CustomArtistImageUtil.Companion.getInstance(App.Companion.getContext()) .hasCustomArtistImage(artist); if (noCustomImage || !hasCustomImage) { - return requestManager.load(new ArtistImage(artist.getName())); + return requestManager.load(new ArtistImage(artist)); } else { return requestManager.load(CustomArtistImageUtil.getFile(artist)); } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index 43d23120d..8052705d7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -14,7 +14,11 @@ package code.name.monkey.retromusic.glide.artistimage +import android.content.ContentResolver import android.content.Context +import androidx.core.net.toFile +import code.name.monkey.retromusic.repository.ArtistRepository +import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Data import code.name.monkey.retromusic.network.DeezerService import code.name.monkey.retromusic.util.MusicUtil @@ -30,11 +34,20 @@ import com.bumptech.glide.load.model.stream.StreamModelLoader import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import java.io.File import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit -class ArtistImage(val artistName: String) +class ArtistImage { + val artist: Artist + val artistName: String + + constructor(artist: Artist) { + this.artist = artist + this.artistName = artist.name + } +} class ArtistImageFetcher( private val context: Context, @@ -85,9 +98,12 @@ class ArtistImageFetcher( val glideUrl = GlideUrl(imageUrl) urlFetcher = urlLoader.getResourceFetcher(glideUrl, width, height) urlFetcher?.loadData(priority) - } else null + } else { + // Image not found by deezer. Use an album cover instead + getFallbackAlbumImage() + } } catch (e: Exception) { - null + getFallbackAlbumImage() } } else return null } @@ -102,6 +118,11 @@ class ArtistImageFetcher( else -> "" } } + + private fun getFallbackAlbumImage(): InputStream? { + val imageUri = MusicUtil.getMediaStoreAlbumCoverUri(model.artist.safeGetFirstAlbum().id) + return context.getContentResolver().openInputStream(imageUri) + } } class ArtistImageLoader( From 24f91a48dbe8c2d009d1c2d132a7025f23b6e1ab Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 14 Sep 2020 02:07:11 -0400 Subject: [PATCH 04/72] Removed unnecessary imports --- .../monkey/retromusic/glide/artistimage/ArtistImageLoader.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index 8052705d7..f9992d121 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -14,10 +14,7 @@ package code.name.monkey.retromusic.glide.artistimage -import android.content.ContentResolver import android.content.Context -import androidx.core.net.toFile -import code.name.monkey.retromusic.repository.ArtistRepository import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Data import code.name.monkey.retromusic.network.DeezerService @@ -34,7 +31,6 @@ import com.bumptech.glide.load.model.stream.StreamModelLoader import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import java.io.File import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit From 03723b62eb90e1a58a0edad07ca4d1aba2d875da Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 17 Sep 2020 23:26:59 +0530 Subject: [PATCH 05/72] Now playing state management --- .../base/AbsSlidingMusicPanelActivity.kt | 83 +++++++++++++++++-- .../fragments/DetailListFragment.kt | 64 +++++--------- .../retromusic/fragments/LibraryViewModel.kt | 54 ++++++++++-- .../fragments/albums/AlbumDetailsFragment.kt | 6 +- .../artists/ArtistDetailsFragment.kt | 13 +-- .../fragments/genres/GenreDetailsFragment.kt | 7 +- .../playlists/PlaylistDetailsFragment.kt | 7 +- .../fragments/search/SearchViewModel.kt | 2 +- .../retromusic/state/NowPlayingPanelState.kt | 8 ++ 9 files changed, 177 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/state/NowPlayingPanelState.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 729adee2f..c78c75da1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -9,7 +9,7 @@ import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.core.view.ViewCompat import androidx.core.view.isVisible -import androidx.lifecycle.Observer +import androidx.transition.TransitionManager import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R @@ -22,6 +22,7 @@ import code.name.monkey.retromusic.fragments.NowPlayingScreen import code.name.monkey.retromusic.fragments.NowPlayingScreen.* import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.CategoryInfo +import code.name.monkey.retromusic.state.NowPlayingPanelState.* import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.views.BottomNavigationBarTinted import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -75,13 +76,11 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { setupSlidingUpPanel() setupBottomSheet() - - libraryViewModel.paletteColorLiveData.observe(this, Observer { - this.paletteColor = it - onPaletteColorChanged() - }) + updatePanelState() + updateColor() } + fun getBottomSheetBehavior() = behavior private fun setupBottomSheet() { @@ -201,7 +200,11 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this) - hideBottomBar(false) + if (bottomNavigationView.isVisible) { + libraryViewModel.setPanelState(COLLAPSED_WITH) + } else { + libraryViewModel.setPanelState(COLLAPSED_WITHOUT) + } } }) } // don't call hideBottomBar(true) here as it causes a bug with the SlidingUpPanelLayout @@ -209,7 +212,16 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun onQueueChanged() { super.onQueueChanged() - hideBottomBar(MusicPlayerRemote.playingQueue.isEmpty()) + val isEmpty = MusicPlayerRemote.playingQueue.isEmpty() + if (isEmpty) { + libraryViewModel.setPanelState(HIDE) + } else { + if (bottomNavigationView.isVisible) { + libraryViewModel.setPanelState(EXPAND) + } else { + libraryViewModel.setPanelState(COLLAPSED_WITHOUT) + } + } } override fun onBackPressed() { @@ -308,4 +320,59 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomNavigationView.hide() } } + + private fun updateColor() { + libraryViewModel.paletteColor.observe(this, { color -> + this.paletteColor = color + onPaletteColorChanged() + }) + } + + private fun updatePanelState() { + libraryViewModel.panelState.observe(this, { state -> + when (state) { + EXPAND -> { + println("EXPAND") + expandPanel() + } + HIDE -> { + println("HIDE") + behavior.isHideable = true + behavior.peekHeight = 0 + collapsePanel() + ViewCompat.setElevation(slidingPanel, 0f) + ViewCompat.setElevation(bottomNavigationView, 10f) + } + COLLAPSED_WITH -> { + println("COLLAPSED_WITH") + TransitionManager.beginDelayedTransition(mainContent) + bottomNavigationView.isVisible = true + val heightOfBar = bottomNavigationView.height + ViewCompat.setElevation(bottomNavigationView, 10f) + ViewCompat.setElevation(slidingPanel, 10f) + behavior.isHideable = false + behavior.peekHeight = (heightOfBar * 2) - 24 + } + COLLAPSED_WITHOUT -> { + println("COLLAPSED_WITHOUT") + TransitionManager.beginDelayedTransition(mainContent) + TransitionManager.beginDelayedTransition(slidingPanel) + val heightOfBar = bottomNavigationView.height + bottomNavigationView.isVisible = false + ViewCompat.setElevation(bottomNavigationView, 10f) + ViewCompat.setElevation(slidingPanel, 10f) + behavior.isHideable = false + behavior.peekHeight = heightOfBar - 24 + } + else -> { + println("ELSE") + behavior.isHideable = true + behavior.peekHeight = 0 + collapsePanel() + ViewCompat.setElevation(slidingPanel, 0f) + ViewCompat.setElevation(bottomNavigationView, 10f) + } + } + }) + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index 6c7230a52..c08611daf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -18,23 +17,19 @@ import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist -import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.android.synthetic.main.fragment_playlist_detail.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.sharedViewModel class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_detail), ArtistClickListener, AlbumClickListener { private val args by navArgs() - private val repository by inject() + private val libraryViewModel by sharedViewModel() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.setSupportActionBar(toolbar) - mainActivity.hideBottomBarVisibility(false) progressIndicator.hide() when (args.type) { TOP_ARTISTS -> { @@ -67,10 +62,9 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - lifecycleScope.launch(IO) { - val songs = repository.recentSongs() - withContext(Main) { songAdapter.swapDataSet(songs) } - } + libraryViewModel.recentSongs().observe(viewLifecycleOwner, Observer { songs -> + songAdapter.swapDataSet(songs) + }) } private fun topPlayed() { @@ -84,12 +78,10 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - lifecycleScope.launch(IO) { - val songs = repository.playCountSongs().map { - it.toSong() - } - withContext(Main) { songAdapter.swapDataSet(songs) } - } + libraryViewModel.playCountSongs().observe(viewLifecycleOwner, Observer { songs -> + songAdapter.swapDataSet(songs) + }) + } private fun loadHistory() { @@ -104,7 +96,7 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - repository.observableHistorySongs().observe(viewLifecycleOwner, Observer { + libraryViewModel.observableHistorySongs().observe(viewLifecycleOwner, Observer { val songs = it.map { historyEntity -> historyEntity.toSong() } songAdapter.swapDataSet(songs) }) @@ -121,8 +113,7 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - repository.favorites().observe(viewLifecycleOwner, Observer { - println(it.size) + libraryViewModel.favorites().observe(viewLifecycleOwner, { val songs = it.map { songEntity -> songEntity.toSong() } songAdapter.swapDataSet(songs) }) @@ -130,31 +121,22 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de private fun loadArtists(title: Int, type: Int) { toolbar.setTitle(title) - lifecycleScope.launch(IO) { - val artists = - if (type == TOP_ARTISTS) repository.topArtists() else repository.recentArtists() - withContext(Main) { - recyclerView.apply { - adapter = artistAdapter(artists) - layoutManager = gridLayoutManager() - } + libraryViewModel.artists(type).observe(viewLifecycleOwner, { artists -> + recyclerView.apply { + adapter = artistAdapter(artists) + layoutManager = gridLayoutManager() } - } + }) } private fun loadAlbums(title: Int, type: Int) { toolbar.setTitle(title) - lifecycleScope.launch(IO) { - val albums = - if (type == TOP_ALBUMS) repository.topAlbums() else repository.recentAlbums() - withContext(Main) { - recyclerView.apply { - adapter = albumAdapter(albums) - layoutManager = gridLayoutManager() - - } + libraryViewModel.albums(type).observe(viewLifecycleOwner, { albums -> + recyclerView.apply { + adapter = albumAdapter(albums) + layoutManager = gridLayoutManager() } - } + }) } private fun artistAdapter(artists: List): ArtistAdapter = ArtistAdapter( diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 3999ee708..07ed2acd8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -1,17 +1,20 @@ package code.name.monkey.retromusic.fragments -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* +import code.name.monkey.retromusic.RECENT_ALBUMS +import code.name.monkey.retromusic.RECENT_ARTISTS +import code.name.monkey.retromusic.TOP_ALBUMS +import code.name.monkey.retromusic.TOP_ARTISTS import code.name.monkey.retromusic.db.PlaylistEntity import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.db.SongEntity +import code.name.monkey.retromusic.db.toSong import code.name.monkey.retromusic.fragments.ReloadType.* import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.MusicServiceEventListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch @@ -19,7 +22,7 @@ class LibraryViewModel( private val repository: RealRepository ) : ViewModel(), MusicServiceEventListener { - private val paletteColor = MutableLiveData() + private val _paletteColor = MutableLiveData() private val albums = MutableLiveData>() private val songs = MutableLiveData>() private val artists = MutableLiveData>() @@ -28,12 +31,17 @@ class LibraryViewModel( private val genres = MutableLiveData>() private val home = MutableLiveData>() - val paletteColorLiveData: LiveData = paletteColor + val paletteColor: LiveData = _paletteColor + val panelState: MutableLiveData = MutableLiveData() init { fetchHomeSections() } + fun setPanelState(state: NowPlayingPanelState) { + panelState.postValue(state) + } + private fun loadLibraryContent() = viewModelScope.launch(IO) { fetchHomeSections() fetchSongs() @@ -132,7 +140,7 @@ class LibraryViewModel( } fun updateColor(newColor: Int) { - paletteColor.postValue(newColor) + _paletteColor.postValue(newColor) } override fun onMediaStoreChanged() { @@ -232,6 +240,38 @@ class LibraryViewModel( fetchPlaylists() loadLibraryContent() } + + fun recentSongs(): LiveData> = liveData { + emit(repository.recentSongs()) + } + + fun playCountSongs(): LiveData> = liveData { + emit(repository.playCountSongs().map { + it.toSong() + }) + } + + fun observableHistorySongs() = repository.observableHistorySongs() + + fun favorites() = repository.favorites() + + fun artists(type: Int): LiveData> = liveData { + when (type) { + TOP_ARTISTS -> emit(repository.topArtists()) + RECENT_ARTISTS -> { + emit(repository.recentArtists()) + } + } + } + + fun albums(type: Int): LiveData> = liveData { + when (type) { + TOP_ALBUMS -> emit(repository.topAlbums()) + RECENT_ALBUMS -> { + emit(repository.recentAlbums()) + } + } + } } enum class ReloadType { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index bade59ff5..e1e4fc878 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -29,6 +29,7 @@ import code.name.monkey.retromusic.dialogs.DeleteSongsDialog import code.name.monkey.retromusic.extensions.applyColor import code.name.monkey.retromusic.extensions.applyOutlineColor import code.name.monkey.retromusic.extensions.show +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.ArtistGlideRequest @@ -41,6 +42,7 @@ import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmAlbum import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil @@ -52,6 +54,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -63,6 +66,7 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det private val detailsViewModel by viewModel { parametersOf(arguments.extraAlbumId) } + private val libraryViewModel by sharedViewModel() private lateinit var simpleSongAdapter: SimpleSongAdapter private lateinit var album: Album @@ -73,7 +77,7 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) - mainActivity.hideBottomBarVisibility(false) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) toolbar.title = " " diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt index 877def068..45469fb51 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt @@ -27,6 +27,7 @@ import code.name.monkey.retromusic.extensions.applyColor import code.name.monkey.retromusic.extensions.applyOutlineColor import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.extensions.showToast +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.ArtistGlideRequest @@ -36,6 +37,7 @@ import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmArtist import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.CustomArtistImageUtil import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil @@ -47,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -58,7 +61,7 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d private val detailsViewModel: ArtistDetailsViewModel by viewModel { parametersOf(arguments.extraArtistId) } - + private val libraryViewModel by sharedViewModel() private lateinit var artist: Artist private lateinit var songAdapter: SimpleSongAdapter private lateinit var albumAdapter: HorizontalAlbumAdapter @@ -70,17 +73,17 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.setSupportActionBar(toolbar) - mainActivity.hideBottomBarVisibility(false) + toolbar.title = null setupRecyclerView() + postponeEnterTransition() detailsViewModel.getArtist().observe(viewLifecycleOwner, Observer { - showArtist(it) startPostponedEnterTransition() + showArtist(it) }) - - playAction.apply { setOnClickListener { MusicPlayerRemote.openQueue(artist.songs, 0, true) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt index fef2df722..8a5f7d522 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt @@ -12,11 +12,14 @@ import androidx.recyclerview.widget.RecyclerView import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.extensions.dipToPix +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.helper.menu.GenreMenuHelper import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.android.synthetic.main.fragment_playlist_detail.* +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -26,7 +29,7 @@ class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_ private val detailsViewModel: GenreDetailsViewModel by viewModel { parametersOf(arguments.extraGenre) } - + private val libraryViewModel by sharedViewModel() private lateinit var genre: Genre private lateinit var songAdapter: SongAdapter @@ -35,7 +38,7 @@ class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_ setHasOptionsMenu(true) mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) - mainActivity.hideBottomBarVisibility(false) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) progressIndicator.hide() setupRecyclerView() detailsViewModel.getSongs().observe(viewLifecycleOwner, androidx.lifecycle.Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt index a47e29a6d..641cab49d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt @@ -14,14 +14,17 @@ import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.db.toSongs import code.name.monkey.retromusic.extensions.dipToPix +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.PlaylistsUtil import com.h6ah4i.android.widget.advrecyclerview.animator.RefactoredDefaultItemAnimator import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import kotlinx.android.synthetic.main.fragment_playlist_detail.* +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -30,7 +33,7 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli private val viewModel: PlaylistDetailsViewModel by viewModel { parametersOf(arguments.extraPlaylist) } - + private val libraryViewModel by sharedViewModel() private lateinit var playlist: PlaylistWithSongs private lateinit var adapter: SongAdapter @@ -40,9 +43,9 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.addMusicServiceEventListener(viewModel) mainActivity.setSupportActionBar(toolbar) - mainActivity.hideBottomBarVisibility(false) playlist = arguments.extraPlaylist toolbar.title = playlist.playlistEntity.playlistName diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt index 506810236..adeb09c89 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt @@ -15,6 +15,6 @@ class SearchViewModel(private val realRepository: RealRepository) : ViewModel() fun search(query: String?) = viewModelScope.launch(IO) { val result = realRepository.search(query) - results.value = result + results.postValue(result) } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/state/NowPlayingPanelState.kt b/app/src/main/java/code/name/monkey/retromusic/state/NowPlayingPanelState.kt new file mode 100644 index 000000000..b142f1436 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/state/NowPlayingPanelState.kt @@ -0,0 +1,8 @@ +package code.name.monkey.retromusic.state + +enum class NowPlayingPanelState { + EXPAND, + COLLAPSED_WITH, + COLLAPSED_WITHOUT, + HIDE, +} \ No newline at end of file From 76e55ba07931664438b6f5e805412335594ad451 Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 13 Sep 2020 21:42:12 -0400 Subject: [PATCH 06/72] Minimum working prototype --- .../java/code/name/monkey/retromusic/Constants.kt | 1 + .../monkey/retromusic/fragments/LibraryViewModel.kt | 11 +++++++++-- .../monkey/retromusic/repository/ArtistRepository.kt | 5 ++++- .../name/monkey/retromusic/util/PreferenceUtil.kt | 9 ++++++++- app/src/main/res/values/strings.xml | 4 ++++ app/src/main/res/xml/pref_ui.xml | 7 +++++++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 4e3a7a241..f0ce5dbd1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -123,6 +123,7 @@ const val AUTO_DOWNLOAD_IMAGES_POLICY = "auto_download_images_policy" const val START_DIRECTORY = "start_directory" const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval" const val LOCK_SCREEN = "lock_screen" +const val ALBUM_ARTISTS_ONLY = "album_artists_only" const val ALBUM_DETAIL_SONG_SORT_ORDER = "album_detail_song_sort_order" const val LYRICS_OPTIONS = "lyrics_tab_position" const val CHOOSE_EQUALIZER = "choose_equalizer" diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index a44cd7bb0..f3b609b45 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -13,6 +13,7 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.MusicServiceEventListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.repository.RealRepository +import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch @@ -91,8 +92,14 @@ class LibraryViewModel( } private fun fetchArtists() { - viewModelScope.launch(IO) { - artists.postValue(repository.fetchArtists()) + if (PreferenceUtil.albumArtistsOnly) { + viewModelScope.launch(IO) { + artists.postValue(repository.albumArtists()) + } + } else { + viewModelScope.launch(IO) { + artists.postValue(repository.fetchArtists()) + } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt index 6ecf6509b..314d13316 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt @@ -67,7 +67,10 @@ class RealArtistRepository( getSongLoaderSortOrder() ) ) - return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)) + + val sortString = if (PreferenceUtil.artistSortOrder.contains("DESC")) String.CASE_INSENSITIVE_ORDER.reversed() else String.CASE_INSENSITIVE_ORDER + + return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)).sortedWith(compareBy(sortString) { it.name }) } override fun artists(query: String): List { diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt index 883336841..22221639e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt @@ -122,6 +122,13 @@ object PreferenceUtil { "only_wifi" ) + var albumArtistsOnly + get() = sharedPreferences.getBoolean( + ALBUM_ARTISTS_ONLY, + false + ) + set(value) = sharedPreferences.edit { putBoolean(ALBUM_ARTISTS_ONLY, value) } + var albumDetailSongSortOrder get() = sharedPreferences.getStringOrDefault( ALBUM_DETAIL_SONG_SORT_ORDER, @@ -150,7 +157,7 @@ object PreferenceUtil { var artistSortOrder get() = sharedPreferences.getStringOrDefault( ARTIST_SORT_ORDER, - AlbumSortOrder.ALBUM_A_Z + ArtistSortOrder.ARTIST_A_Z ) set(value) = sharedPreferences.edit { putString(ARTIST_SORT_ORDER, value) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 018bdc9f8..be213e122 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ Sort order + Album artists only + Tag editor Toggle favorite @@ -517,6 +519,7 @@ Snow fall effect + Show Album Artists in the Artist category Use the currently playing song album cover as the lockscreen wallpaper Lower the volume when a system sound is played or a notification is received The content of blacklisted folders is hidden from your library. @@ -546,6 +549,7 @@ Shuffle mode will turn off when playing a new list of songs If enough space is available, show volume controls in the now playing screen + Navigate by Album Artist Show album cover Album cover theme Album cover skip diff --git a/app/src/main/res/xml/pref_ui.xml b/app/src/main/res/xml/pref_ui.xml index 364756242..50c1e7d29 100644 --- a/app/src/main/res/xml/pref_ui.xml +++ b/app/src/main/res/xml/pref_ui.xml @@ -45,6 +45,13 @@ android:negativeButtonText="@null" android:positiveButtonText="@null" android:title="@string/pref_title_tab_text_mode" /> + + Date: Sun, 20 Sep 2020 17:23:32 -0400 Subject: [PATCH 07/72] Fix incorrect loading from #895 --- .../code/name/monkey/retromusic/repository/ArtistRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt index 314d13316..907bf49c9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt @@ -90,7 +90,7 @@ class RealArtistRepository( .map { val currentAlbums = it.value if (albums.isNotEmpty()) { - Artist(currentAlbums[0].id, currentAlbums) + Artist(currentAlbums[0].artistId, currentAlbums) } else { Artist.empty } From 6747af076fd29a3b01bc1e7ecd4f2d2a46f98237 Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 21 Sep 2020 00:17:53 -0400 Subject: [PATCH 08/72] Various artists is working --- .../name/monkey/retromusic/model/Artist.kt | 4 +++ .../retromusic/repository/ArtistRepository.kt | 25 +++++++++++++++---- .../name/monkey/retromusic/util/MusicUtil.kt | 7 ++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt b/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt index 98fe4209e..fbfde54c8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt @@ -27,6 +27,8 @@ data class Artist( val name = safeGetFirstAlbum().safeGetFirstSong().albumArtist return if (MusicUtil.isArtistNameUnknown(name)) { UNKNOWN_ARTIST_DISPLAY_NAME + } else if (MusicUtil.isVariousArtists(name)) { + VARIOUS_ARTISTS_DISPLAY_NAME } else safeGetFirstAlbum().safeGetFirstSong().artistName } @@ -51,6 +53,8 @@ data class Artist( companion object { const val UNKNOWN_ARTIST_DISPLAY_NAME = "Unknown Artist" + const val VARIOUS_ARTISTS_DISPLAY_NAME = "Various Artists" + const val VARIOUS_ARTISTS_ID : Long = -2 val empty = Artist(-1, emptyList()) } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt index 907bf49c9..d78df6d1f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/ArtistRepository.kt @@ -40,6 +40,19 @@ class RealArtistRepository( PreferenceUtil.artistSongSortOrder } override fun artist(artistId: Long): Artist { + if (artistId == Artist.VARIOUS_ARTISTS_ID) { + // Get Various Artists + val songs = songRepository.songs( + songRepository.makeSongCursor( + null, + null, + getSongLoaderSortOrder() + ) + ) + val albums = albumRepository.splitIntoAlbums(songs).filter { it.albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME } + return Artist(Artist.VARIOUS_ARTISTS_ID, albums) + } + val songs = songRepository.songs( songRepository.makeSongCursor( AudioColumns.ARTIST_ID + "=?", @@ -68,9 +81,7 @@ class RealArtistRepository( ) ) - val sortString = if (PreferenceUtil.artistSortOrder.contains("DESC")) String.CASE_INSENSITIVE_ORDER.reversed() else String.CASE_INSENSITIVE_ORDER - - return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)).sortedWith(compareBy(sortString) { it.name }) + return splitIntoAlbumArtists(albumRepository.splitIntoAlbums(songs)) } override fun artists(query: String): List { @@ -89,8 +100,12 @@ class RealArtistRepository( return albums.groupBy { it.albumArtist } .map { val currentAlbums = it.value - if (albums.isNotEmpty()) { - Artist(currentAlbums[0].artistId, currentAlbums) + if (currentAlbums.isNotEmpty()) { + if (currentAlbums[0].albumArtist == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) { + Artist(Artist.VARIOUS_ARTISTS_ID, currentAlbums) + } else { + Artist(currentAlbums[0].artistId, currentAlbums) + } } else { Artist.empty } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt index 0af7cb916..e267966a4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt @@ -299,6 +299,13 @@ object MusicUtil : KoinComponent { return tempName == "unknown" || tempName == "" } + fun isVariousArtists(artistName: String): Boolean { + if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) { + return true + } + return false + } + fun isFavorite(context: Context, song: Song): Boolean { return PlaylistsUtil .doPlaylistContains(context, getFavoritesPlaylist(context).id, song.id) From 991c4ee36e5449b5d5c200799631241c129b0e5c Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 21 Sep 2020 00:31:53 -0400 Subject: [PATCH 09/72] Got code cleanup happy... all fixed --- .../main/java/code/name/monkey/retromusic/util/MusicUtil.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt index e267966a4..645b88433 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt @@ -299,7 +299,10 @@ object MusicUtil : KoinComponent { return tempName == "unknown" || tempName == "" } - fun isVariousArtists(artistName: String): Boolean { + fun isVariousArtists(artistName: String?): Boolean { + if (TextUtils.isEmpty(artistName)) { + return false + } if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) { return true } From 5d1a75bc1384ee32d296b0d076753733d503f6b2 Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 21 Sep 2020 00:39:26 -0400 Subject: [PATCH 10/72] Fixing bug when preference is not set --- .../main/java/code/name/monkey/retromusic/model/Artist.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt b/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt index fbfde54c8..bb4fd0b1c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/Artist.kt @@ -15,6 +15,7 @@ package code.name.monkey.retromusic.model import code.name.monkey.retromusic.util.MusicUtil +import code.name.monkey.retromusic.util.PreferenceUtil import java.util.* data class Artist( @@ -25,10 +26,11 @@ data class Artist( val name: String get() { val name = safeGetFirstAlbum().safeGetFirstSong().albumArtist + if (PreferenceUtil.albumArtistsOnly && MusicUtil.isVariousArtists(name)) { + return VARIOUS_ARTISTS_DISPLAY_NAME + } return if (MusicUtil.isArtistNameUnknown(name)) { UNKNOWN_ARTIST_DISPLAY_NAME - } else if (MusicUtil.isVariousArtists(name)) { - VARIOUS_ARTISTS_DISPLAY_NAME } else safeGetFirstAlbum().safeGetFirstSong().artistName } From 40ceb57c45d51687ab5af82e9e1b8b796f0abcdd Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 21 Sep 2020 16:17:08 +0530 Subject: [PATCH 11/72] Update AlbumDetailsFragment.kt --- .../retromusic/fragments/albums/AlbumDetailsFragment.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index e730a025c..a793f0e36 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -77,6 +77,11 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det private val savedSortOrder: String get() = PreferenceUtil.albumDetailSongSortOrder + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedElementEnterTransition = MaterialContainerTransform().apply { @@ -88,7 +93,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) toolbar.title = " " From c495c66a38a23a588c1c1ed28a82e61aa4895d13 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 22 Sep 2020 13:50:43 +0530 Subject: [PATCH 12/72] Added animation collapse when navigating to details --- .../base/AbsSlidingMusicPanelActivity.kt | 29 +++++++------------ .../retromusic/extensions/ViewExtensions.kt | 20 +++++++++++++ .../artists/ArtistDetailsFragment.kt | 1 - .../fragments/library/LibraryFragment.kt | 7 ++++- .../fragments/search/SearchFragment.kt | 7 +++-- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index c78c75da1..e7b2355e4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -9,12 +9,13 @@ import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.core.view.ViewCompat import androidx.core.view.isVisible -import androidx.transition.TransitionManager import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior import code.name.monkey.retromusic.extensions.hide +import code.name.monkey.retromusic.extensions.peekHeightAnimate +import code.name.monkey.retromusic.extensions.translateXAnimate import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment @@ -217,7 +218,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { libraryViewModel.setPanelState(HIDE) } else { if (bottomNavigationView.isVisible) { - libraryViewModel.setPanelState(EXPAND) + libraryViewModel.setPanelState(COLLAPSED_WITH) } else { libraryViewModel.setPanelState(COLLAPSED_WITHOUT) } @@ -301,12 +302,6 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } } - fun hideBottomNavigation() { - behavior.isHideable = true - behavior.peekHeight = 0 - hideBottomBarVisibility(false) - } - fun updateTabs() { bottomNavigationView.menu.clear() val currentTabs: List = PreferenceUtil.libraryCategory @@ -330,6 +325,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { private fun updatePanelState() { libraryViewModel.panelState.observe(this, { state -> + val isQueueEmpty = MusicPlayerRemote.playingQueue.isEmpty() when (state) { EXPAND -> { println("EXPAND") @@ -337,32 +333,29 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } HIDE -> { println("HIDE") - behavior.isHideable = true - behavior.peekHeight = 0 - collapsePanel() ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) + behavior.isHideable = true + behavior.peekHeightAnimate(0) + collapsePanel() } COLLAPSED_WITH -> { println("COLLAPSED_WITH") - TransitionManager.beginDelayedTransition(mainContent) - bottomNavigationView.isVisible = true val heightOfBar = bottomNavigationView.height ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) behavior.isHideable = false - behavior.peekHeight = (heightOfBar * 2) - 24 + behavior.peekHeightAnimate(if(isQueueEmpty) 0 else (heightOfBar * 2) - 24) + bottomNavigationView.translateXAnimate(0f) } COLLAPSED_WITHOUT -> { println("COLLAPSED_WITHOUT") - TransitionManager.beginDelayedTransition(mainContent) - TransitionManager.beginDelayedTransition(slidingPanel) val heightOfBar = bottomNavigationView.height - bottomNavigationView.isVisible = false ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) behavior.isHideable = false - behavior.peekHeight = heightOfBar - 24 + behavior.peekHeightAnimate(if(isQueueEmpty) 0 else heightOfBar - 24) + bottomNavigationView.translateXAnimate(heightOfBar.toFloat()) } else -> { println("ELSE") diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 0f9e30e8a..3c224ed6a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -14,6 +14,7 @@ package code.name.monkey.retromusic.extensions +import android.animation.ObjectAnimator import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -21,6 +22,7 @@ import android.widget.EditText import androidx.annotation.LayoutRes import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.TintHelper +import com.google.android.material.bottomsheet.BottomSheetBehavior @Suppress("UNCHECKED_CAST") fun ViewGroup.inflate(@LayoutRes layout: Int): T { @@ -45,3 +47,21 @@ fun EditText.appHandleColor(): EditText { TintHelper.colorHandles(this, ThemeStore.accentColor(context)) return this } + + +fun View.translateXAnimate(value: Float) { + ObjectAnimator.ofFloat(this, "translationY", value) + .apply { + duration = 300 + start() + } +} + +fun BottomSheetBehavior<*>.peekHeightAnimate(value: Int) { + ObjectAnimator.ofInt(this, "peekHeight", value) + .apply { + duration = 300 + start() + } +} + diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt index 65cc18c2d..1caec7e6f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt @@ -73,7 +73,6 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d setHasOptionsMenu(true) libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.setSupportActionBar(toolbar) - toolbar.title = null setupRecyclerView() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 086e0a09b..545f06c40 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -14,17 +14,22 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog import code.name.monkey.retromusic.extensions.findNavController +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment +import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.android.synthetic.main.fragment_library.* +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.lang.String class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { + private val libraryViewModel by sharedViewModel() + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) retainInstance = true - mainActivity.hideBottomBarVisibility(true) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null toolbar.setNavigationOnClickListener { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt index a900341e2..14fdc80e9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt @@ -20,10 +20,13 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.SearchAdapter import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.showToast +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment +import code.name.monkey.retromusic.state.NowPlayingPanelState import com.google.android.material.textfield.TextInputEditText import kotlinx.android.synthetic.main.fragment_search.* import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.util.* import kotlin.collections.ArrayList @@ -33,6 +36,7 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa const val REQ_CODE_SPEECH_INPUT = 9001 } + private val libraryViewModel by sharedViewModel() private val viewModel: SearchViewModel by inject() private lateinit var searchAdapter: SearchAdapter private var query: String? = null @@ -40,8 +44,7 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) mainActivity.setSupportActionBar(toolbar) - mainActivity.hideBottomBarVisibility(false) - + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) setupRecyclerView() keyboardPopup.accentColor() searchView.addTextChangedListener(this) From 7c0b3ee82ccfd2b52e75665e7c308bea80909643 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 22 Sep 2020 15:00:00 +0530 Subject: [PATCH 13/72] Update ViewExtensions.kt --- .../name/monkey/retromusic/extensions/ViewExtensions.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 3c224ed6a..d32583fbc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.annotation.LayoutRes +import androidx.core.animation.doOnEnd import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.TintHelper import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -54,6 +55,12 @@ fun View.translateXAnimate(value: Float) { .apply { duration = 300 start() + doOnEnd { + + if (value != 0f) { + this@translateXAnimate.hide() + } + } } } From 3f368e186b6de1c21dbf5c0ba8c302cec9b11728 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 02:25:12 +0530 Subject: [PATCH 14/72] Updated code Added Fragment options Renamed Interfaces Rolled back to previous implementaion for Now playing to handle backpress --- .../code/name/monkey/retromusic/MainModule.kt | 5 - .../base/AbsMusicServiceActivity.kt | 18 +- .../base/AbsSlidingMusicPanelActivity.kt | 162 ++++++++++------- .../retromusic/adapter/SearchAdapter.kt | 2 +- .../retromusic/adapter/SongFileAdapter.kt | 20 +-- .../retromusic/adapter/album/AlbumAdapter.kt | 6 +- .../adapter/album/HorizontalAlbumAdapter.kt | 6 +- .../adapter/artist/ArtistAdapter.kt | 6 +- .../adapter/base/AbsMultiSelectAdapter.java | 16 +- .../adapter/playlist/PlaylistAdapter.kt | 6 +- .../adapter/song/AbsOffsetSongAdapter.kt | 6 +- .../song/OrderablePlaylistSongAdapter.kt | 6 +- .../adapter/song/PlaylistSongAdapter.kt | 6 +- .../adapter/song/ShuffleButtonSongAdapter.kt | 6 +- .../adapter/song/SimpleSongAdapter.kt | 6 +- .../retromusic/adapter/song/SongAdapter.kt | 6 +- .../retromusic/dialogs/SavePlaylistDialog.kt | 13 +- .../retromusic/extensions/ViewExtensions.kt | 7 - .../retromusic/fragments/LibraryViewModel.kt | 29 ++- .../fragments/albums/AlbumDetailsFragment.kt | 2 +- .../fragments/albums/AlbumDetailsViewModel.kt | 4 +- .../artists/ArtistDetailsViewModel.kt | 4 +- .../fragments/base/AbsMusicServiceFragment.kt | 14 +- .../fragments/base/AbsPlayerFragment.kt | 4 +- .../fragments/folder/FoldersFragment.java | 49 ++++-- .../fragments/genres/GenreDetailsViewModel.kt | 4 +- .../fragments/library/LibraryFragment.kt | 1 - .../player/NowPlayingPlayerFragment.kt | 42 ----- .../fragments/player/color/ColorFragment.kt | 2 +- .../player/full/FullPlayerFragment.kt | 16 +- .../playlists/PlaylistDetailsViewModel.kt | 4 +- .../fragments/search/SearchFragment.kt | 28 +-- .../fragments/search/SearchViewModel.kt | 20 --- .../retromusic/helper/menu/SongMenuHelper.kt | 6 +- .../{CabHolder.kt => ICabHolder.kt} | 2 +- .../{Callbacks.kt => ICallbacks.kt} | 2 +- ...s.kt => IMainActivityFragmentCallbacks.kt} | 2 +- ...tener.kt => IMusicServiceEventListener.kt} | 2 +- ...eColorHolder.kt => IPaletteColorHolder.kt} | 2 +- .../retromusic/repository/GenreRepository.kt | 5 +- .../monkey/retromusic/util/PlaylistsUtil.java | 2 +- .../layout/fragment_now_playing_player.xml | 13 -- .../res/layout/sliding_music_panel_layout.xml | 3 +- app/src/main/res/menu/menu_main.xml | 1 + app/src/main/res/navigation/now_playing.xml | 165 ------------------ 45 files changed, 274 insertions(+), 457 deletions(-) delete mode 100644 app/src/main/java/code/name/monkey/retromusic/fragments/player/NowPlayingPlayerFragment.kt delete mode 100644 app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt rename app/src/main/java/code/name/monkey/retromusic/interfaces/{CabHolder.kt => ICabHolder.kt} (97%) rename app/src/main/java/code/name/monkey/retromusic/interfaces/{Callbacks.kt => ICallbacks.kt} (92%) rename app/src/main/java/code/name/monkey/retromusic/interfaces/{MainActivityFragmentCallbacks.kt => IMainActivityFragmentCallbacks.kt} (93%) rename app/src/main/java/code/name/monkey/retromusic/interfaces/{MusicServiceEventListener.kt => IMusicServiceEventListener.kt} (95%) rename app/src/main/java/code/name/monkey/retromusic/interfaces/{PaletteColorHolder.kt => IPaletteColorHolder.kt} (95%) delete mode 100644 app/src/main/res/layout/fragment_now_playing_player.xml delete mode 100644 app/src/main/res/navigation/now_playing.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt index 3e068f205..4552702ae 100644 --- a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt +++ b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt @@ -12,7 +12,6 @@ import code.name.monkey.retromusic.fragments.albums.AlbumDetailsViewModel import code.name.monkey.retromusic.fragments.artists.ArtistDetailsViewModel import code.name.monkey.retromusic.fragments.genres.GenreDetailsViewModel import code.name.monkey.retromusic.fragments.playlists.PlaylistDetailsViewModel -import code.name.monkey.retromusic.fragments.search.SearchViewModel import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.network.* import code.name.monkey.retromusic.repository.* @@ -189,10 +188,6 @@ private val viewModules = module { genre ) } - - viewModel { - SearchViewModel(get()) - } } val appModules = listOf(mainModule, dataModule, viewModules, networkModule, roomModule) \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt index 1e9efee87..7b13a4bca 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.lifecycleScope import code.name.monkey.retromusic.R import code.name.monkey.retromusic.db.toPlayCount import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.repository.RealRepository import code.name.monkey.retromusic.service.MusicService.* import kotlinx.coroutines.Dispatchers @@ -17,9 +17,9 @@ import org.koin.android.ext.android.inject import java.lang.ref.WeakReference import java.util.* -abstract class AbsMusicServiceActivity : AbsBaseActivity(), MusicServiceEventListener { +abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventListener { - private val mMusicServiceEventListeners = ArrayList() + private val mMusicServiceEventListeners = ArrayList() private val repository: RealRepository by inject() private var serviceToken: MusicPlayerRemote.ServiceToken? = null private var musicStateReceiver: MusicStateReceiver? = null @@ -49,15 +49,15 @@ abstract class AbsMusicServiceActivity : AbsBaseActivity(), MusicServiceEventLis } } - fun addMusicServiceEventListener(listener: MusicServiceEventListener?) { - if (listener != null) { - mMusicServiceEventListeners.add(listener) + fun addMusicServiceEventListener(listenerI: IMusicServiceEventListener?) { + if (listenerI != null) { + mMusicServiceEventListeners.add(listenerI) } } - fun removeMusicServiceEventListener(listener: MusicServiceEventListener?) { - if (listener != null) { - mMusicServiceEventListeners.remove(listener) + fun removeMusicServiceEventListener(listenerI: IMusicServiceEventListener?) { + if (listenerI != null) { + mMusicServiceEventListeners.remove(listenerI) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index e7b2355e4..638118361 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -8,25 +8,41 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.core.view.ViewCompat +import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.extensions.peekHeightAnimate -import code.name.monkey.retromusic.extensions.translateXAnimate import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment import code.name.monkey.retromusic.fragments.NowPlayingScreen import code.name.monkey.retromusic.fragments.NowPlayingScreen.* +import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment +import code.name.monkey.retromusic.fragments.player.adaptive.AdaptiveFragment +import code.name.monkey.retromusic.fragments.player.blur.BlurPlayerFragment +import code.name.monkey.retromusic.fragments.player.card.CardFragment +import code.name.monkey.retromusic.fragments.player.cardblur.CardBlurFragment +import code.name.monkey.retromusic.fragments.player.circle.CirclePlayerFragment +import code.name.monkey.retromusic.fragments.player.color.ColorFragment +import code.name.monkey.retromusic.fragments.player.fit.FitFragment +import code.name.monkey.retromusic.fragments.player.flat.FlatPlayerFragment +import code.name.monkey.retromusic.fragments.player.full.FullPlayerFragment +import code.name.monkey.retromusic.fragments.player.gradient.GradientPlayerFragment +import code.name.monkey.retromusic.fragments.player.material.MaterialFragment +import code.name.monkey.retromusic.fragments.player.normal.PlayerFragment +import code.name.monkey.retromusic.fragments.player.plain.PlainPlayerFragment +import code.name.monkey.retromusic.fragments.player.simple.SimplePlayerFragment +import code.name.monkey.retromusic.fragments.player.tiny.TinyPlayerFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.state.NowPlayingPanelState.* import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.views.BottomNavigationBarTinted -import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.* import kotlinx.android.synthetic.main.sliding_music_panel_layout.* import org.koin.androidx.viewmodel.ext.android.viewModel @@ -36,9 +52,9 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } protected val libraryViewModel by viewModel() - private lateinit var behavior: RetroBottomSheetBehavior + private lateinit var bottomSheetBehavior: RetroBottomSheetBehavior private var miniPlayerFragment: MiniPlayerFragment? = null - private var cps: NowPlayingScreen? = null + private var nowPlayingScreen: NowPlayingScreen? = null private var navigationBarColor: Int = 0 private var taskColor: Int = 0 private var lightStatusBar: Boolean = false @@ -46,9 +62,9 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { private var paletteColor: Int = Color.WHITE protected abstract fun createContentView(): View private val panelState: Int - get() = behavior.state + get() = bottomSheetBehavior.state - private val bottomSheetCallbackList = object : BottomSheetBehavior.BottomSheetCallback() { + private val bottomSheetCallbackList = object : BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { setMiniPlayerAlphaProgress(slideOffset) @@ -56,14 +72,14 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun onStateChanged(bottomSheet: View, newState: Int) { when (newState) { - BottomSheetBehavior.STATE_EXPANDED -> { + STATE_EXPANDED -> { onPanelExpanded() } - BottomSheetBehavior.STATE_COLLAPSED -> { + STATE_COLLAPSED -> { onPanelCollapsed() } else -> { - + println("Do something") } } } @@ -75,34 +91,29 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { setContentView(createContentView()) chooseFragmentForTheme() setupSlidingUpPanel() - setupBottomSheet() updatePanelState() updateColor() } - fun getBottomSheetBehavior() = behavior + fun getBottomSheetBehavior() = bottomSheetBehavior private fun setupBottomSheet() { - behavior = BottomSheetBehavior.from(slidingPanel) as RetroBottomSheetBehavior - behavior.addBottomSheetCallback(bottomSheetCallbackList) - - if (behavior.state == BottomSheetBehavior.STATE_EXPANDED) { - setMiniPlayerAlphaProgress(1f) - } + bottomSheetBehavior = from(slidingPanel) as RetroBottomSheetBehavior + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallbackList) } override fun onResume() { super.onResume() - if (cps != PreferenceUtil.nowPlayingScreen) { + if (nowPlayingScreen != PreferenceUtil.nowPlayingScreen) { postRecreate() } } override fun onDestroy() { super.onDestroy() - behavior.removeBottomSheetCallback(bottomSheetCallbackList) + bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallbackList) } protected fun wrapSlidingMusicPanel(@LayoutRes resId: Int): View { @@ -115,17 +126,14 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } fun collapsePanel() { - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - setMiniPlayerAlphaProgress(0f) + bottomSheetBehavior.state = STATE_COLLAPSED } fun expandPanel() { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - setMiniPlayerAlphaProgress(1f) + bottomSheetBehavior.state = STATE_EXPANDED } private fun setMiniPlayerAlphaProgress(progress: Float) { - if (miniPlayerFragment?.view == null) return val alpha = 1 - progress miniPlayerFragment?.view?.alpha = alpha miniPlayerFragment?.view?.visibility = if (alpha == 0f) View.GONE else View.VISIBLE @@ -151,8 +159,8 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun onGlobalLayout() { slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this) when (panelState) { - BottomSheetBehavior.STATE_EXPANDED -> onPanelExpanded() - BottomSheetBehavior.STATE_COLLAPSED -> onPanelCollapsed() + STATE_EXPANDED -> onPanelExpanded() + STATE_COLLAPSED -> onPanelCollapsed() else -> { //playerFragment!!.onHide() } @@ -175,24 +183,20 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { val isBottomBarVisible = bottomNavigationView.isVisible if (hide) { - behavior.isHideable = true - behavior.peekHeight = 0 + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.peekHeight = 0 collapsePanel() ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) } else { ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) - behavior.isHideable = false - behavior.peekHeight = (if (isBottomBarVisible) heightOfBar * 2 else heightOfBar) - 24 + bottomSheetBehavior.isHideable = false + bottomSheetBehavior.peekHeight = + (if (isBottomBarVisible) heightOfBar * 2 else heightOfBar) - 24 } } - private fun chooseFragmentForTheme() { - cps = PreferenceUtil.nowPlayingScreen - miniPlayerFragment = whichFragment(R.id.miniPlayerFragment) - miniPlayerFragment?.view?.setOnClickListener { expandPanel() } - } override fun onServiceConnected() { super.onServiceConnected() @@ -229,8 +233,9 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { if (!handleBackPress()) super.onBackPressed() } - open fun handleBackPress(): Boolean { - if (panelState == BottomSheetBehavior.STATE_EXPANDED) { + private fun handleBackPress(): Boolean { + if (bottomSheetBehavior.peekHeight != 0 && playerFragment!!.onBackPressed()) return true + if (panelState == STATE_EXPANDED) { collapsePanel() return true } @@ -238,27 +243,27 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } private fun onPaletteColorChanged() { - if (panelState == BottomSheetBehavior.STATE_EXPANDED) { + if (panelState == STATE_EXPANDED) { super.setTaskDescriptionColor(paletteColor) val isColorLight = ColorUtil.isColorLight(paletteColor) - if (PreferenceUtil.isAdaptiveColor && (cps == Normal || cps == Flat)) { + if (PreferenceUtil.isAdaptiveColor && (nowPlayingScreen == Normal || nowPlayingScreen == Flat)) { super.setLightNavigationBar(true) super.setLightStatusbar(isColorLight) - } else if (cps == Card || cps == Blur || cps == BlurCard) { + } else if (nowPlayingScreen == Card || nowPlayingScreen == Blur || nowPlayingScreen == BlurCard) { super.setLightStatusbar(false) super.setLightNavigationBar(true) super.setNavigationbarColor(Color.BLACK) - } else if (cps == Color || cps == Tiny || cps == Gradient) { + } else if (nowPlayingScreen == Color || nowPlayingScreen == Tiny || nowPlayingScreen == Gradient) { super.setNavigationbarColor(paletteColor) super.setLightNavigationBar(isColorLight) super.setLightStatusbar(isColorLight) - } else if (cps == Full) { + } else if (nowPlayingScreen == Full) { super.setNavigationbarColor(paletteColor) super.setLightNavigationBar(isColorLight) super.setLightStatusbar(false) - } else if (cps == Classic) { + } else if (nowPlayingScreen == Classic) { super.setLightStatusbar(false) - } else if (cps == Fit) { + } else if (nowPlayingScreen == Fit) { super.setLightStatusbar(false) } else { super.setLightStatusbar( @@ -276,28 +281,28 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun setLightStatusbar(enabled: Boolean) { lightStatusBar = enabled - if (panelState == BottomSheetBehavior.STATE_COLLAPSED) { + if (panelState == STATE_COLLAPSED) { super.setLightStatusbar(enabled) } } override fun setLightNavigationBar(enabled: Boolean) { lightNavigationBar = enabled - if (panelState == BottomSheetBehavior.STATE_COLLAPSED) { + if (panelState == STATE_COLLAPSED) { super.setLightNavigationBar(enabled) } } override fun setNavigationbarColor(color: Int) { navigationBarColor = color - if (panelState == BottomSheetBehavior.STATE_COLLAPSED) { + if (panelState == STATE_COLLAPSED) { super.setNavigationbarColor(color) } } override fun setTaskDescriptionColor(color: Int) { taskColor = color - if (panelState == BottomSheetBehavior.STATE_COLLAPSED) { + if (panelState == STATE_COLLAPSED) { super.setTaskDescriptionColor(color) } } @@ -335,32 +340,38 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { println("HIDE") ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) - behavior.isHideable = true - behavior.peekHeightAnimate(0) - collapsePanel() + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.setPeekHeight(0, true) + bottomSheetBehavior.state = STATE_COLLAPSED } COLLAPSED_WITH -> { println("COLLAPSED_WITH") val heightOfBar = bottomNavigationView.height ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) - behavior.isHideable = false - behavior.peekHeightAnimate(if(isQueueEmpty) 0 else (heightOfBar * 2) - 24) - bottomNavigationView.translateXAnimate(0f) + bottomSheetBehavior.isHideable = false + bottomSheetBehavior.setPeekHeight( + if (isQueueEmpty) 0 else (heightOfBar * 2) - 24, + true + ) + bottomNavigationView.isVisible = true } COLLAPSED_WITHOUT -> { println("COLLAPSED_WITHOUT") val heightOfBar = bottomNavigationView.height ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) - behavior.isHideable = false - behavior.peekHeightAnimate(if(isQueueEmpty) 0 else heightOfBar - 24) - bottomNavigationView.translateXAnimate(heightOfBar.toFloat()) + bottomSheetBehavior.isHideable = false + bottomSheetBehavior.setPeekHeight( + if (isQueueEmpty) 0 else heightOfBar - 24, + true + ) + bottomNavigationView.isGone = true } else -> { println("ELSE") - behavior.isHideable = true - behavior.peekHeight = 0 + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.peekHeight = 0 collapsePanel() ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) @@ -368,4 +379,35 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } }) } + private var playerFragment: AbsPlayerFragment? = null + private fun chooseFragmentForTheme() { + nowPlayingScreen = PreferenceUtil.nowPlayingScreen + + val fragment: Fragment = when (nowPlayingScreen) { + Blur -> BlurPlayerFragment() + Adaptive -> AdaptiveFragment() + Normal -> PlayerFragment() + Card -> CardFragment() + BlurCard -> CardBlurFragment() + Fit -> FitFragment() + Flat -> FlatPlayerFragment() + Full -> FullPlayerFragment() + Plain -> PlainPlayerFragment() + Simple -> SimplePlayerFragment() + Material -> MaterialFragment() + Color -> ColorFragment() + Gradient -> GradientPlayerFragment() + Tiny -> TinyPlayerFragment() + //PEAK -> PeakPlayerFragment() + Circle -> CirclePlayerFragment() + else -> PlayerFragment() + } // must implement AbsPlayerFragment + supportFragmentManager.beginTransaction().replace(R.id.playerFragmentContainer, fragment) + .commit() + supportFragmentManager.executePendingTransactions() + + playerFragment = supportFragmentManager.findFragmentById(R.id.playerFragmentContainer) as AbsPlayerFragment + miniPlayerFragment = whichFragment(R.id.miniPlayerFragment) + miniPlayerFragment?.view?.setOnClickListener { expandPanel() } + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt index 97e2ada1d..21d1b92a1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt @@ -25,7 +25,7 @@ class SearchAdapter( private var dataSet: List ) : RecyclerView.Adapter() { - fun swapDataSet(dataSet: MutableList) { + fun swapDataSet(dataSet: List) { this.dataSet = dataSet notifyDataSetChanged() } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt index 0f073f182..511336227 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt @@ -26,8 +26,8 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder import code.name.monkey.retromusic.glide.audiocover.AudioFileCover -import code.name.monkey.retromusic.interfaces.CabHolder -import code.name.monkey.retromusic.interfaces.Callbacks +import code.name.monkey.retromusic.interfaces.ICabHolder +import code.name.monkey.retromusic.interfaces.ICallbacks import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide @@ -43,10 +43,10 @@ class SongFileAdapter( private val activity: AppCompatActivity, private var dataSet: List, private val itemLayoutRes: Int, - private val callbacks: Callbacks?, - cabHolder: CabHolder? + private val ICallbacks: ICallbacks?, + ICabHolder: ICabHolder? ) : AbsMultiSelectAdapter( - activity, cabHolder, R.menu.menu_media_selection + activity, ICabHolder, R.menu.menu_media_selection ), PopupTextProvider { init { @@ -136,8 +136,8 @@ class SongFileAdapter( } override fun onMultipleItemAction(menuItem: MenuItem, selection: List) { - if (callbacks == null) return - callbacks.onMultipleItemAction(menuItem, selection as ArrayList) + if (ICallbacks == null) return + ICallbacks.onMultipleItemAction(menuItem, selection as ArrayList) } override fun getPopupText(position: Int): String { @@ -152,11 +152,11 @@ class SongFileAdapter( inner class ViewHolder(itemView: View) : MediaEntryViewHolder(itemView) { init { - if (menu != null && callbacks != null) { + if (menu != null && ICallbacks != null) { menu?.setOnClickListener { v -> val position = layoutPosition if (isPositionInRange(position)) { - callbacks.onFileMenuClicked(dataSet[position], v) + ICallbacks.onFileMenuClicked(dataSet[position], v) } } } @@ -171,7 +171,7 @@ class SongFileAdapter( if (isInQuickSelectMode) { toggleChecked(position) } else { - callbacks?.onFileSelected(dataSet[position]) + ICallbacks?.onFileSelected(dataSet[position]) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt index 73c071452..2d3896cbc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt @@ -16,7 +16,7 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.menu.SongsMenuHelper -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil @@ -29,11 +29,11 @@ open class AlbumAdapter( protected val activity: FragmentActivity, var dataSet: List, protected var itemLayoutRes: Int, - cabHolder: CabHolder?, + ICabHolder: ICabHolder?, private val albumClickListener: AlbumClickListener? ) : AbsMultiSelectAdapter( activity, - cabHolder, + ICabHolder, R.menu.menu_media_selection ), PopupTextProvider { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt index 8198a957e..7ff459ec3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt @@ -7,7 +7,7 @@ import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.HorizontalAdapterHelper -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor @@ -16,10 +16,10 @@ import com.bumptech.glide.Glide class HorizontalAlbumAdapter( activity: FragmentActivity, dataSet: List, - cabHolder: CabHolder?, + ICabHolder: ICabHolder?, albumClickListener: AlbumClickListener ) : AlbumAdapter( - activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, cabHolder, albumClickListener + activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, ICabHolder, albumClickListener ) { override fun createViewHolder(view: View, viewType: Int): ViewHolder { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt index 5f55486e1..7f32e9848 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt @@ -16,7 +16,7 @@ import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.glide.ArtistGlideRequest import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.menu.SongsMenuHelper -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil @@ -29,10 +29,10 @@ class ArtistAdapter( val activity: FragmentActivity, var dataSet: List, var itemLayoutRes: Int, - cabHolder: CabHolder?, + ICabHolder: ICabHolder?, private val artistClickListener: ArtistClickListener ) : AbsMultiSelectAdapter( - activity, cabHolder, R.menu.menu_media_selection + activity, ICabHolder, R.menu.menu_media_selection ), PopupTextProvider { init { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java index bca248f6d..0e2667ae6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java @@ -15,21 +15,21 @@ import java.util.ArrayList; import java.util.List; import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.interfaces.CabHolder; +import code.name.monkey.retromusic.interfaces.ICabHolder; public abstract class AbsMultiSelectAdapter extends RecyclerView.Adapter implements MaterialCab.Callback { @Nullable - private final CabHolder cabHolder; + private final ICabHolder ICabHolder; private final Context context; private MaterialCab cab; private List checked; private int menuRes; - public AbsMultiSelectAdapter(@NonNull Context context, @Nullable CabHolder cabHolder, @MenuRes int menuRes) { - this.cabHolder = cabHolder; + public AbsMultiSelectAdapter(@NonNull Context context, @Nullable ICabHolder ICabHolder, @MenuRes int menuRes) { + this.ICabHolder = ICabHolder; checked = new ArrayList<>(); this.menuRes = menuRes; this.context = context; @@ -59,7 +59,7 @@ public abstract class AbsMultiSelectAdapter, private var itemLayoutRes: Int, - cabHolder: CabHolder? + ICabHolder: ICabHolder? ) : AbsMultiSelectAdapter( activity, - cabHolder, + ICabHolder, R.menu.menu_playlists_selection ) { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt index d1141341c..9904bc5b5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt @@ -7,15 +7,15 @@ import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song abstract class AbsOffsetSongAdapter( activity: AppCompatActivity, dataSet: MutableList, @LayoutRes itemLayoutRes: Int, - cabHolder: CabHolder? -) : SongAdapter(activity, dataSet, itemLayoutRes, cabHolder) { + ICabHolder: ICabHolder? +) : SongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongAdapter.ViewHolder { if (viewType == OFFSET_ITEM) { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt index a806c72a0..64f9aa199 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt @@ -9,7 +9,7 @@ import code.name.monkey.retromusic.db.PlaylistEntity import code.name.monkey.retromusic.db.toSongEntity import code.name.monkey.retromusic.db.toSongs import code.name.monkey.retromusic.dialogs.RemoveSongFromPlaylistDialog -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.PlaylistSong import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.ViewUtil @@ -23,13 +23,13 @@ class OrderablePlaylistSongAdapter( activity: FragmentActivity, dataSet: ArrayList, itemLayoutRes: Int, - cabHolder: CabHolder?, + ICabHolder: ICabHolder?, private val onMoveItemListener: OnMoveItemListener? ) : SongAdapter( activity, dataSet, itemLayoutRes, - cabHolder + ICabHolder ), DraggableItemAdapter { init { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt index 7d73b9529..27f7edec6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt @@ -8,7 +8,7 @@ import androidx.navigation.findNavController import code.name.monkey.retromusic.EXTRA_ALBUM_ID import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song import com.google.android.material.button.MaterialButton @@ -16,8 +16,8 @@ open class PlaylistSongAdapter( activity: AppCompatActivity, dataSet: MutableList, itemLayoutRes: Int, - cabHolder: CabHolder? -) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, cabHolder) { + ICabHolder: ICabHolder? +) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) { init { this.setMultiSelectMenuRes(R.menu.menu_cannot_delete_single_songs_playlist_songs_selection) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt index 4f3935ad4..794cb8a4c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt @@ -4,7 +4,7 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song import com.google.android.material.button.MaterialButton @@ -12,8 +12,8 @@ class ShuffleButtonSongAdapter( activity: AppCompatActivity, dataSet: MutableList, itemLayoutRes: Int, - cabHolder: CabHolder? -) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, cabHolder) { + ICabHolder: ICabHolder? +) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) { override fun createViewHolder(view: View): SongAdapter.ViewHolder { return ViewHolder(view) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt index d75400289..7bfc4a1c1 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt @@ -3,7 +3,7 @@ package code.name.monkey.retromusic.adapter.song import android.view.LayoutInflater import android.view.ViewGroup import androidx.fragment.app.FragmentActivity -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import java.util.* @@ -12,8 +12,8 @@ class SimpleSongAdapter( context: FragmentActivity, songs: ArrayList, layoutRes: Int, - cabHolder: CabHolder? -) : SongAdapter(context, songs, layoutRes, cabHolder) { + ICabHolder: ICabHolder? +) : SongAdapter(context, songs, layoutRes, ICabHolder) { override fun swapDataSet(dataSet: List) { this.dataSet = dataSet.toMutableList() diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index b7b1f8e45..f7707c5a6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -21,7 +21,7 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.menu.SongMenuHelper import code.name.monkey.retromusic.helper.menu.SongsMenuHelper -import code.name.monkey.retromusic.interfaces.CabHolder +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil @@ -38,11 +38,11 @@ open class SongAdapter( protected val activity: FragmentActivity, var dataSet: MutableList, protected var itemLayoutRes: Int, - cabHolder: CabHolder?, + ICabHolder: ICabHolder?, showSectionName: Boolean = true ) : AbsMultiSelectAdapter( activity, - cabHolder, + ICabHolder, R.menu.menu_media_selection ), MaterialCab.Callback, PopupTextProvider { diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt index dd92a9a2d..bf726887b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt @@ -1,6 +1,7 @@ package code.name.monkey.retromusic.dialogs import android.app.Dialog +import android.media.MediaScannerConnection import android.os.Bundle import android.widget.Toast import androidx.core.os.bundleOf @@ -33,9 +34,15 @@ class SavePlaylistDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { - val playlistWithSongs: PlaylistWithSongs = - extraNotNull(EXTRA_PLAYLIST).value - val file = PlaylistsUtil.savePlaylistWithSongs(requireContext(), playlistWithSongs) + val playlistWithSongs = extraNotNull(EXTRA_PLAYLIST).value + val file = PlaylistsUtil.savePlaylistWithSongs(playlistWithSongs) + MediaScannerConnection.scanFile( + requireActivity(), + arrayOf(file.path), + null + ) { _, _ -> + + } withContext(Dispatchers.Main) { Toast.makeText( requireContext(), diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index d32583fbc..3c224ed6a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -20,7 +20,6 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.annotation.LayoutRes -import androidx.core.animation.doOnEnd import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.TintHelper import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -55,12 +54,6 @@ fun View.translateXAnimate(value: Float) { .apply { duration = 300 start() - doOnEnd { - - if (value != 0f) { - this@translateXAnimate.hide() - } - } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 1c27813cc..248951353 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -8,7 +8,7 @@ import code.name.monkey.retromusic.TOP_ARTISTS import code.name.monkey.retromusic.db.* import code.name.monkey.retromusic.fragments.ReloadType.* import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.repository.RealRepository @@ -21,24 +21,21 @@ import kotlinx.coroutines.launch class LibraryViewModel( private val repository: RealRepository -) : ViewModel(), MusicServiceEventListener { +) : ViewModel(), IMusicServiceEventListener { private val _paletteColor = MutableLiveData() + private val home = MutableLiveData>() private val albums = MutableLiveData>() private val songs = MutableLiveData>() private val artists = MutableLiveData>() private val playlists = MutableLiveData>() private val legacyPlaylists = MutableLiveData>() private val genres = MutableLiveData>() - private val home = MutableLiveData>() + private val searchResults = MutableLiveData>() val paletteColor: LiveData = _paletteColor val panelState: MutableLiveData = MutableLiveData() - init { - fetchHomeSections() - } - fun setPanelState(state: NowPlayingPanelState) { panelState.postValue(state) } @@ -52,6 +49,8 @@ class LibraryViewModel( fetchPlaylists() } + fun getSearchResult(): LiveData> = searchResults + fun getSongs(): LiveData> { fetchSongs() return songs @@ -83,6 +82,7 @@ class LibraryViewModel( } fun getHome(): LiveData> { + fetchHomeSections() return home } @@ -134,6 +134,11 @@ class LibraryViewModel( } } + fun search(query: String?) = viewModelScope.launch(IO) { + val result = repository.search(query) + searchResults.postValue(result) + } + fun forceReload(reloadType: ReloadType) = viewModelScope.launch { when (reloadType) { Songs -> fetchSongs() @@ -278,6 +283,16 @@ class LibraryViewModel( } } } + + fun clearSearchResult() { + viewModelScope.launch { + searchResults.postValue(emptyList()) + } + } + + fun artist(artistId: Long): LiveData = liveData { + emit(repository.artistById(artistId)) + } } enum class ReloadType { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index a793f0e36..296d8955b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -133,7 +133,7 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det override fun onDestroy() { super.onDestroy() - playerActivity?.removeMusicServiceEventListener(detailsViewModel) + serviceActivity?.removeMusicServiceEventListener(detailsViewModel) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt index 025449331..f764a98ba 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt @@ -3,7 +3,7 @@ package code.name.monkey.retromusic.fragments.albums import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result @@ -14,7 +14,7 @@ import kotlinx.coroutines.Dispatchers.IO class AlbumDetailsViewModel( private val repository: RealRepository, private val albumId: Long -) : ViewModel(), MusicServiceEventListener { +) : ViewModel(), IMusicServiceEventListener { fun getAlbum(): LiveData = liveData(IO) { emit(repository.albumByIdAsync(albumId)) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt index 1f883600c..26fbc2acb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt @@ -3,7 +3,7 @@ package code.name.monkey.retromusic.fragments.artists import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmArtist @@ -13,7 +13,7 @@ import kotlinx.coroutines.Dispatchers.IO class ArtistDetailsViewModel( private val realRepository: RealRepository, private val artistId: Long -) : ViewModel(), MusicServiceEventListener { +) : ViewModel(), IMusicServiceEventListener { fun getArtist(): LiveData = liveData(IO) { val artist = realRepository.artistById(artistId) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt index 1ce30de03..fcfdc3c25 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt @@ -10,7 +10,7 @@ import androidx.fragment.app.Fragment import androidx.navigation.navOptions import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.RetroUtil import org.jaudiotagger.audio.AudioFileIO @@ -23,7 +23,7 @@ import java.util.* */ open class AbsMusicServiceFragment(@LayoutRes layout: Int) : Fragment(layout), - MusicServiceEventListener { + IMusicServiceEventListener { val navOptions by lazy { navOptions { @@ -40,13 +40,13 @@ open class AbsMusicServiceFragment(@LayoutRes layout: Int) : Fragment(layout), } } - var playerActivity: AbsMusicServiceActivity? = null + var serviceActivity: AbsMusicServiceActivity? = null private set override fun onAttach(context: Context) { super.onAttach(context) try { - playerActivity = context as AbsMusicServiceActivity? + serviceActivity = context as AbsMusicServiceActivity? } catch (e: ClassCastException) { throw RuntimeException(context.javaClass.simpleName + " must be an instance of " + AbsMusicServiceActivity::class.java.simpleName) } @@ -54,17 +54,17 @@ open class AbsMusicServiceFragment(@LayoutRes layout: Int) : Fragment(layout), override fun onDetach() { super.onDetach() - playerActivity = null + serviceActivity = null } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - playerActivity?.addMusicServiceEventListener(this) + serviceActivity?.addMusicServiceEventListener(this) } override fun onDestroyView() { super.onDestroyView() - playerActivity?.removeMusicServiceEventListener(this) + serviceActivity?.removeMusicServiceEventListener(this) } override fun onPlayingMetaChanged() { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt index e2b499a00..8f067d478 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt @@ -31,7 +31,7 @@ import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.PaletteColorHolder +import code.name.monkey.retromusic.interfaces.IPaletteColorHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.repository.RealRepository @@ -47,7 +47,7 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.io.FileNotFoundException abstract class AbsPlayerFragment(@LayoutRes layout: Int) : AbsMainActivityFragment(layout), - Toolbar.OnMenuItemClickListener, PaletteColorHolder, PlayerAlbumCoverFragment.Callbacks { + Toolbar.OnMenuItemClickListener, IPaletteColorHolder, PlayerAlbumCoverFragment.Callbacks { private var playerAlbumCoverFragment: PlayerAlbumCoverFragment? = null protected val libraryViewModel by sharedViewModel() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index 4d5a0d099..329172b7f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -21,6 +21,8 @@ import android.os.Bundle; import android.os.Environment; import android.text.Html; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -41,6 +43,8 @@ import com.afollestad.materialcab.MaterialCab; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.io.FileFilter; import java.io.IOException; @@ -59,9 +63,9 @@ import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment; import code.name.monkey.retromusic.helper.MusicPlayerRemote; import code.name.monkey.retromusic.helper.menu.SongMenuHelper; import code.name.monkey.retromusic.helper.menu.SongsMenuHelper; -import code.name.monkey.retromusic.interfaces.CabHolder; -import code.name.monkey.retromusic.interfaces.Callbacks; -import code.name.monkey.retromusic.interfaces.MainActivityFragmentCallbacks; +import code.name.monkey.retromusic.interfaces.ICabHolder; +import code.name.monkey.retromusic.interfaces.ICallbacks; +import code.name.monkey.retromusic.interfaces.IMainActivityFragmentCallbacks; import code.name.monkey.retromusic.misc.DialogAsyncTask; import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; import code.name.monkey.retromusic.misc.WrappedAsyncTaskLoader; @@ -76,10 +80,10 @@ import code.name.monkey.retromusic.views.ScrollingViewOnApplyWindowInsetsListene import me.zhanghai.android.fastscroll.FastScroller; public class FoldersFragment extends AbsMainActivityFragment implements - MainActivityFragmentCallbacks, - CabHolder, + IMainActivityFragmentCallbacks, + ICabHolder, BreadCrumbLayout.SelectionCallback, - Callbacks, + ICallbacks, LoaderManager.LoaderCallbacks> { public static final String TAG = FoldersFragment.class.getSimpleName(); @@ -137,6 +141,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements } } + @NonNull @Override public View onCreateView(@NonNull LayoutInflater inflater, @@ -159,13 +164,12 @@ public class FoldersFragment extends AbsMainActivityFragment implements @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - + setHasOptionsMenu(true); if (savedInstanceState == null) { - //noinspection ConstantConditions setCrumb(new BreadCrumbLayout.Crumb(FileUtil.safeGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), true); } else { breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS)); - getLoaderManager().initLoader(LOADER_ID, null, this); + LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this); } } @@ -181,7 +185,6 @@ public class FoldersFragment extends AbsMainActivityFragment implements if (breadCrumbs != null) { outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper()); } - } @Override @@ -209,7 +212,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements } @Override - public void onFileMenuClicked(final File file, View view) { + public void onFileMenuClicked(final File file, @NotNull View view) { PopupMenu popupMenu = new PopupMenu(getActivity(), view); if (file.isDirectory()) { popupMenu.inflate(R.menu.menu_item_directory); @@ -222,7 +225,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements case R.id.action_delete_from_device: new ListSongsAsyncTask(getActivity(), null, (songs, extra) -> { if (!songs.isEmpty()) { - SongsMenuHelper.INSTANCE.handleMenuClick(getActivity(), songs, itemId); + SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId); } }).execute(new ListSongsAsyncTask.LoadingInfo(toList(file), AUDIO_FILE_FILTER, getFileComparator())); @@ -256,7 +259,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements case R.id.action_set_as_ringtone: case R.id.action_delete_from_device: new ListSongsAsyncTask(getActivity(), null, - (songs, extra) -> SongMenuHelper.INSTANCE.handleMenuClick(getActivity(), + (songs, extra) -> SongMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs.get(0), itemId)) .execute(new ListSongsAsyncTask.LoadingInfo(toList(file), AUDIO_FILE_FILTER, getFileComparator())); @@ -273,7 +276,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements } @Override - public void onFileSelected(File file) { + public void onFileSelected(@NotNull File file) { file = tryGetCanonicalFile(file); // important as we compare the path value later if (file.isDirectory()) { setCrumb(new BreadCrumbLayout.Crumb(file), true); @@ -319,13 +322,23 @@ public class FoldersFragment extends AbsMainActivityFragment implements } @Override - public void onMultipleItemAction(MenuItem item, ArrayList files) { + public void onMultipleItemAction(MenuItem item, @NotNull ArrayList files) { final int itemId = item.getItemId(); new ListSongsAsyncTask(getActivity(), null, - (songs, extra) -> SongsMenuHelper.INSTANCE.handleMenuClick(getActivity(), songs, itemId)) + (songs, extra) -> SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId)) .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.add(0, R.id.action_scan, 0, R.string.scan_media).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.removeItem(R.id.action_grid_size); + menu.removeItem(R.id.action_layout_type); + menu.removeItem(R.id.action_sort_order); + } + @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { @@ -360,7 +373,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements @NonNull @Override - public MaterialCab openCab(int menuRes, MaterialCab.Callback callback) { + public MaterialCab openCab(int menuRes, @NotNull MaterialCab.Callback callback) { if (cab != null && cab.isActive()) { cab.finish(); } @@ -438,7 +451,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements if (addToHistory) { breadCrumbs.addHistory(crumb); } - getLoaderManager().restartLoader(LOADER_ID, null, this); + LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this); } private void setUpAdapter() { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt index 7641784cd..ceb419d83 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.RealRepository @@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext class GenreDetailsViewModel( private val realRepository: RealRepository, private val genre: Genre -) : ViewModel(), MusicServiceEventListener { +) : ViewModel(), IMusicServiceEventListener { private val _playListSongs = MutableLiveData>() private val _genre = MutableLiveData().apply { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 545f06c40..818f52aa7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -19,7 +19,6 @@ import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.android.synthetic.main.fragment_library.* import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import java.lang.String class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/NowPlayingPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/NowPlayingPlayerFragment.kt deleted file mode 100644 index 3de375140..000000000 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/NowPlayingPlayerFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package code.name.monkey.retromusic.fragments.player - -import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.navigation.NavController -import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.extensions.findNavController -import code.name.monkey.retromusic.fragments.NowPlayingScreen.* -import code.name.monkey.retromusic.util.PreferenceUtil - -class NowPlayingPlayerFragment : Fragment(R.layout.fragment_now_playing_player) { - companion object { - const val TAG = "NowPlaying" - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - val navController = findNavController(R.id.playerFragmentContainer) - updateNowPlaying(navController) - } - - private fun updateNowPlaying(navController: NavController) { - when (PreferenceUtil.nowPlayingScreen) { - Adaptive -> navController.navigate(R.id.adaptiveFragment) - Blur -> navController.navigate(R.id.blurPlayerFragment) - BlurCard -> navController.navigate(R.id.cardBlurFragment) - Card -> navController.navigate(R.id.cardFragment) - Circle -> navController.navigate(R.id.circlePlayerFragment) - Classic -> navController.navigate(R.id.classicPlayerFragment) - Color -> navController.navigate(R.id.colorFragment) - Fit -> navController.navigate(R.id.fitFragment) - Flat -> navController.navigate(R.id.flatPlayerFragment) - Full -> navController.navigate(R.id.fullPlayerFragment) - Gradient -> navController.navigate(R.id.gradientPlayerFragment) - Material -> navController.navigate(R.id.materialFragment) - Plain -> navController.navigate(R.id.plainPlayerFragment) - Simple -> navController.navigate(R.id.simplePlayerFragment) - Tiny -> navController.navigate(R.id.tinyPlayerFragment) - else -> navController.navigate(R.id.playerFragment) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt index ef3b3b5c4..ab8d70e72 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/color/ColorFragment.kt @@ -37,7 +37,7 @@ class ColorFragment : AbsPlayerFragment(R.layout.fragment_color_player) { navigationColor = color.backgroundColor colorGradientBackground?.setBackgroundColor(color.backgroundColor) - playerActivity?.setLightNavigationBar(ColorUtil.isColorLight(color.backgroundColor)) + serviceActivity?.setLightNavigationBar(ColorUtil.isColorLight(color.backgroundColor)) Handler().post { ToolbarContentTintHelper.colorizeToolbar( playerToolbar, diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt index 9a2f23c38..f68d538aa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt @@ -8,7 +8,6 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.core.os.bundleOf -import androidx.lifecycle.lifecycleScope import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.EXTRA_ARTIST_ID import code.name.monkey.retromusic.R @@ -25,18 +24,12 @@ import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics import code.name.monkey.retromusic.model.lyrics.Lyrics -import code.name.monkey.retromusic.repository.ArtistRepository import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.fragment_full.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full), MusicProgressViewUpdateHelper.Callback { - private val artistRepository by inject() private lateinit var lyricsLayout: FrameLayout private lateinit var lyricsLine1: TextView private lateinit var lyricsLine2: TextView @@ -222,9 +215,8 @@ class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full), } private fun updateArtistImage() { - lifecycleScope.launch { - val artist = artistRepository.artist(MusicPlayerRemote.currentSong.artistId) - withContext(Dispatchers.Main) { + libraryViewModel.artist(MusicPlayerRemote.currentSong.artistId) + .observe(viewLifecycleOwner, { artist -> ArtistGlideRequest.Builder.from(Glide.with(requireContext()), artist) .generatePalette(requireContext()) .build() @@ -233,8 +225,8 @@ class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full), } }) - } - } + }) + } override fun onQueueChanged() { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt index ac498b4e2..d35a90718 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt @@ -5,14 +5,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.db.SongEntity -import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.RealRepository class PlaylistDetailsViewModel( private val realRepository: RealRepository, private var playlist: PlaylistWithSongs -) : ViewModel(), MusicServiceEventListener { +) : ViewModel(), IMusicServiceEventListener { private val _playListSongs = MutableLiveData>() private val _playlist = MutableLiveData().apply { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt index 14fdc80e9..f2dd37a34 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt @@ -12,20 +12,19 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat.getSystemService import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.SearchAdapter import code.name.monkey.retromusic.extensions.accentColor +import code.name.monkey.retromusic.extensions.dipToPix import code.name.monkey.retromusic.extensions.showToast import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.state.NowPlayingPanelState import com.google.android.material.textfield.TextInputEditText import kotlinx.android.synthetic.main.fragment_search.* -import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.util.* import kotlin.collections.ArrayList @@ -37,38 +36,37 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa } private val libraryViewModel by sharedViewModel() - private val viewModel: SearchViewModel by inject() private lateinit var searchAdapter: SearchAdapter private var query: String? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainActivity.setSupportActionBar(toolbar) libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + mainActivity.setSupportActionBar(toolbar) + libraryViewModel.clearSearchResult(); setupRecyclerView() - keyboardPopup.accentColor() searchView.addTextChangedListener(this) voiceSearch.setOnClickListener { startMicSearch() } clearText.setOnClickListener { searchView.clearText() } - keyboardPopup.setOnClickListener { - val inputManager = - getSystemService( + keyboardPopup.apply { + accentColor() + setOnClickListener { + val inputManager = getSystemService( requireContext(), InputMethodManager::class.java ) - inputManager?.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT) + inputManager?.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT) + } } - if (savedInstanceState != null) { query = savedInstanceState.getString(QUERY) } - - viewModel.getSearchResult().observe(viewLifecycleOwner, Observer { + libraryViewModel.getSearchResult().observe(viewLifecycleOwner, { showData(it) }) } - private fun showData(data: MutableList) { + private fun showData(data: List) { if (data.isNotEmpty()) { searchAdapter.swapDataSet(data) } else { @@ -83,6 +81,8 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa override fun onChanged() { super.onChanged() empty.isVisible = searchAdapter.itemCount < 1 + val height = dipToPix(52f) + recyclerView.setPadding(0, 0, 0, height.toInt()) } }) recyclerView.apply { @@ -118,7 +118,7 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa TransitionManager.beginDelayedTransition(appBarLayout) voiceSearch.isGone = query.isNotEmpty() clearText.isVisible = query.isNotEmpty() - viewModel.search(query) + libraryViewModel.search(query) } private fun startMicSearch() { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt deleted file mode 100644 index adeb09c89..000000000 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchViewModel.kt +++ /dev/null @@ -1,20 +0,0 @@ -package code.name.monkey.retromusic.fragments.search - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import code.name.monkey.retromusic.repository.RealRepository -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.launch - -class SearchViewModel(private val realRepository: RealRepository) : ViewModel() { - private val results = MutableLiveData>() - - fun getSearchResult(): LiveData> = results - - fun search(query: String?) = viewModelScope.launch(IO) { - val result = realRepository.search(query) - results.postValue(result) - } -} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt index 07e590a6c..ac161880c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt @@ -30,7 +30,7 @@ import code.name.monkey.retromusic.dialogs.AddToPlaylistDialog import code.name.monkey.retromusic.dialogs.DeleteSongsDialog import code.name.monkey.retromusic.dialogs.SongDetailDialog import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.interfaces.PaletteColorHolder +import code.name.monkey.retromusic.interfaces.IPaletteColorHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.RealRepository import code.name.monkey.retromusic.util.MusicUtil @@ -90,10 +90,10 @@ object SongMenuHelper : KoinComponent { R.id.action_tag_editor -> { val tagEditorIntent = Intent(activity, SongTagEditorActivity::class.java) tagEditorIntent.putExtra(AbsTagEditorActivity.EXTRA_ID, song.id) - if (activity is PaletteColorHolder) + if (activity is IPaletteColorHolder) tagEditorIntent.putExtra( AbsTagEditorActivity.EXTRA_PALETTE, - (activity as PaletteColorHolder).paletteColor + (activity as IPaletteColorHolder).paletteColor ) activity.startActivity(tagEditorIntent) return true diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/CabHolder.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt similarity index 97% rename from app/src/main/java/code/name/monkey/retromusic/interfaces/CabHolder.kt rename to app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt index 95bb43618..c444933c3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/CabHolder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt @@ -17,7 +17,7 @@ package code.name.monkey.retromusic.interfaces import com.afollestad.materialcab.MaterialCab -interface CabHolder { +interface ICabHolder { fun openCab(menuRes: Int, callback: MaterialCab.Callback): MaterialCab } diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/Callbacks.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt similarity index 92% rename from app/src/main/java/code/name/monkey/retromusic/interfaces/Callbacks.kt rename to app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt index 998e23d78..205970f40 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/Callbacks.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt @@ -4,7 +4,7 @@ import android.view.MenuItem import android.view.View import java.io.File -interface Callbacks { +interface ICallbacks { fun onFileSelected(file: File) fun onFileMenuClicked(file: File, view: View) diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/MainActivityFragmentCallbacks.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt similarity index 93% rename from app/src/main/java/code/name/monkey/retromusic/interfaces/MainActivityFragmentCallbacks.kt rename to app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt index 454d36087..034040143 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/MainActivityFragmentCallbacks.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt @@ -16,6 +16,6 @@ package code.name.monkey.retromusic.interfaces /** * Created by hemanths on 14/08/17. */ -internal interface MainActivityFragmentCallbacks { +interface IMainActivityFragmentCallbacks { fun handleBackPress(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/MusicServiceEventListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt similarity index 95% rename from app/src/main/java/code/name/monkey/retromusic/interfaces/MusicServiceEventListener.kt rename to app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt index a62e7ab76..15e7a0399 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/MusicServiceEventListener.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt @@ -15,7 +15,7 @@ package code.name.monkey.retromusic.interfaces -interface MusicServiceEventListener { +interface IMusicServiceEventListener { fun onServiceConnected() fun onServiceDisconnected() diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/PaletteColorHolder.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt similarity index 95% rename from app/src/main/java/code/name/monkey/retromusic/interfaces/PaletteColorHolder.kt rename to app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt index 31d417cbe..e05349738 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/PaletteColorHolder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt @@ -17,6 +17,6 @@ package code.name.monkey.retromusic.interfaces /** * @author Aidan Follestad (afollestad) */ -interface PaletteColorHolder { +interface IPaletteColorHolder { val paletteColor: Int } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt index 34ce19808..948190438 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt @@ -24,6 +24,7 @@ import code.name.monkey.retromusic.Constants.IS_MUSIC import code.name.monkey.retromusic.Constants.baseProjection import code.name.monkey.retromusic.extensions.getLong import code.name.monkey.retromusic.extensions.getString +import code.name.monkey.retromusic.extensions.getStringOrNull import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil @@ -53,9 +54,9 @@ class RealGenreRepository( private fun getGenreFromCursor(cursor: Cursor): Genre { val id = cursor.getLong(Genres._ID) - val name = cursor.getString(Genres.NAME) + val name = cursor.getStringOrNull(Genres.NAME) val songCount = songs(id).size - return Genre(id, name, songCount) + return Genre(id, name ?: "", songCount) } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java index cfff22d35..14ee82bd7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java @@ -251,7 +251,7 @@ public class PlaylistsUtil { return M3UWriter.write(new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); } - public static File savePlaylistWithSongs(Context context, PlaylistWithSongs playlist) throws IOException { + public static File savePlaylistWithSongs(PlaylistWithSongs playlist) throws IOException { return M3UWriter.writeIO(new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); } diff --git a/app/src/main/res/layout/fragment_now_playing_player.xml b/app/src/main/res/layout/fragment_now_playing_player.xml deleted file mode 100644 index 2b7ec4529..000000000 --- a/app/src/main/res/layout/fragment_now_playing_player.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/sliding_music_panel_layout.xml b/app/src/main/res/layout/sliding_music_panel_layout.xml index 27f64d37e..43dfbd3cb 100644 --- a/app/src/main/res/layout/sliding_music_panel_layout.xml +++ b/app/src/main/res/layout/sliding_music_panel_layout.xml @@ -21,9 +21,8 @@ app:behavior_peekHeight="0dp" app:layout_behavior="code.name.monkey.retromusic.RetroBottomSheetBehavior"> - diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 83b9ea939..fc06a2b58 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -90,6 +90,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From caefbbbbadda969d2bff6b9dee69316f3586f4df Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 02:40:16 +0530 Subject: [PATCH 15/72] Fixed peak theme --- .../base/AbsSlidingMusicPanelActivity.kt | 51 ++++++++----------- .../retromusic/fragments/NowPlayingScreen.kt | 2 +- .../res/layout/sliding_music_panel_layout.xml | 9 +++- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 638118361..fd58e7b7c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -16,6 +16,7 @@ import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior import code.name.monkey.retromusic.extensions.hide +import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment @@ -34,6 +35,7 @@ import code.name.monkey.retromusic.fragments.player.full.FullPlayerFragment import code.name.monkey.retromusic.fragments.player.gradient.GradientPlayerFragment import code.name.monkey.retromusic.fragments.player.material.MaterialFragment import code.name.monkey.retromusic.fragments.player.normal.PlayerFragment +import code.name.monkey.retromusic.fragments.player.peak.PeakPlayerFragment import code.name.monkey.retromusic.fragments.player.plain.PlainPlayerFragment import code.name.monkey.retromusic.fragments.player.simple.SimplePlayerFragment import code.name.monkey.retromusic.fragments.player.tiny.TinyPlayerFragment @@ -68,6 +70,8 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun onSlide(bottomSheet: View, slideOffset: Float) { setMiniPlayerAlphaProgress(slideOffset) + dimBackground.show() + dimBackground.alpha = slideOffset } override fun onStateChanged(bottomSheet: View, newState: Int) { @@ -77,6 +81,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } STATE_COLLAPSED -> { onPanelCollapsed() + dimBackground.hide() } else -> { println("Do something") @@ -85,6 +90,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } } + fun getBottomSheetBehavior() = bottomSheetBehavior override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,11 +100,14 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { setupBottomSheet() updatePanelState() updateColor() + + val themeColor = ATHUtil.resolveColor(this, android.R.attr.windowBackground, Color.GRAY) + dimBackground.setBackgroundColor(ColorUtil.withAlpha(themeColor, 0.5f)) + dimBackground.setOnClickListener { + libraryViewModel.setPanelState(COLLAPSED_WITH) + } } - - fun getBottomSheetBehavior() = bottomSheetBehavior - private fun setupBottomSheet() { bottomSheetBehavior = from(slidingPanel) as RetroBottomSheetBehavior bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallbackList) @@ -158,6 +167,11 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this) + if (nowPlayingScreen != Peak) { + val params = slidingPanel.layoutParams as ViewGroup.LayoutParams + params.height = ViewGroup.LayoutParams.MATCH_PARENT + slidingPanel.layoutParams = params + } when (panelState) { STATE_EXPANDED -> onPanelExpanded() STATE_COLLAPSED -> onPanelCollapsed() @@ -173,31 +187,6 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { return bottomNavigationView } - fun hideBottomBarVisibility(visible: Boolean) { - bottomNavigationView.isVisible = visible - hideBottomBar(MusicPlayerRemote.playingQueue.isEmpty()) - } - - private fun hideBottomBar(hide: Boolean) { - val heightOfBar = bottomNavigationView.height - val isBottomBarVisible = bottomNavigationView.isVisible - - if (hide) { - bottomSheetBehavior.isHideable = true - bottomSheetBehavior.peekHeight = 0 - collapsePanel() - ViewCompat.setElevation(slidingPanel, 0f) - ViewCompat.setElevation(bottomNavigationView, 10f) - } else { - ViewCompat.setElevation(bottomNavigationView, 10f) - ViewCompat.setElevation(slidingPanel, 10f) - bottomSheetBehavior.isHideable = false - bottomSheetBehavior.peekHeight = - (if (isBottomBarVisible) heightOfBar * 2 else heightOfBar) - 24 - } - } - - override fun onServiceConnected() { super.onServiceConnected() if (MusicPlayerRemote.playingQueue.isNotEmpty()) { @@ -379,6 +368,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } }) } + private var playerFragment: AbsPlayerFragment? = null private fun chooseFragmentForTheme() { nowPlayingScreen = PreferenceUtil.nowPlayingScreen @@ -398,7 +388,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { Color -> ColorFragment() Gradient -> GradientPlayerFragment() Tiny -> TinyPlayerFragment() - //PEAK -> PeakPlayerFragment() + Peak -> PeakPlayerFragment() Circle -> CirclePlayerFragment() else -> PlayerFragment() } // must implement AbsPlayerFragment @@ -406,7 +396,8 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { .commit() supportFragmentManager.executePendingTransactions() - playerFragment = supportFragmentManager.findFragmentById(R.id.playerFragmentContainer) as AbsPlayerFragment + playerFragment = + supportFragmentManager.findFragmentById(R.id.playerFragmentContainer) as AbsPlayerFragment miniPlayerFragment = whichFragment(R.id.miniPlayerFragment) miniPlayerFragment?.view?.setOnClickListener { expandPanel() } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt index 19e085222..522a93291 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt @@ -24,7 +24,7 @@ enum class NowPlayingScreen constructor( Gradient(R.string.gradient, R.drawable.np_gradient, 17), Material(R.string.material, R.drawable.np_material, 11), Normal(R.string.normal, R.drawable.np_normal, 0), - //Peak(R.string.peak, R.drawable.np_peak, 14), + Peak(R.string.peak, R.drawable.np_peak, 14), Plain(R.string.plain, R.drawable.np_plain, 3), Simple(R.string.simple, R.drawable.np_simple, 8), Tiny(R.string.tiny, R.drawable.np_tiny, 7), diff --git a/app/src/main/res/layout/sliding_music_panel_layout.xml b/app/src/main/res/layout/sliding_music_panel_layout.xml index 43dfbd3cb..5a9078614 100644 --- a/app/src/main/res/layout/sliding_music_panel_layout.xml +++ b/app/src/main/res/layout/sliding_music_panel_layout.xml @@ -11,12 +11,19 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + From 40da78af8bf5331f234d061c4b699d18227f596f Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 04:19:05 +0530 Subject: [PATCH 16/72] Update Code rolled back to old Icon Updated pro gaurd rules --- app/build.gradle | 4 ++-- app/proguard-rules.pro | 9 ++++++++- app/src/main/ic_launcher-playstore.png | Bin 51780 -> 22855 bytes .../monkey/retromusic/lyrics/LrcView.java | 7 ++++--- .../retromusic/repository/SongRepository.kt | 7 ++++++- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 5096 -> 3517 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 6059 -> 2256 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5096 -> 3517 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2974 -> 2205 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 3461 -> 1394 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2974 -> 2205 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 7622 -> 5189 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 9034 -> 3273 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7622 -> 5189 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 12857 -> 8281 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 16055 -> 5716 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 12857 -> 8281 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 19153 -> 11968 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 23877 -> 8527 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 19153 -> 11968 bytes gradle.properties | 2 +- 21 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a75cecfd9..dd90ecf6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10438 - versionName '3.5.650' + "_" + getDate() + versionCode 10442 + versionName '3.6.000_hot_fix' + "_" + getDate() multiDexEnabled true diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a7f474df0..950e401f8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -51,4 +51,11 @@ # native *** n*(...); #} -#-keep class org.jaudiotagger.** { *; } \ No newline at end of file +#-keep class org.jaudiotagger.** { *; } +-keepclassmembers enum * { *; } +-keepattributes *Annotation*, Signature, Exception +-keepnames class androidx.navigation.fragment.NavHostFragment +-keepnames class code.name.monkey.retromusic.model.Home +-keep class * extends androidx.fragment.app.Fragment{} +-keepnames class * extends android.os.Parcelable +-keepnames class * extends java.io.Serializable \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index a4a872d3443def0ad45eafc7ef0a11464ad9bc26..9fdd702524da1c8b0b57e5f44cef3f4892fa3908 100644 GIT binary patch literal 22855 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 V3dS@_LI#w)9B}u+J=zm`@n59HOz!{y literal 51780 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 T?Qhut_~B^pw(0&x#<~9s8$~Us diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java index 050bac4c7..63c4b8cfc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java @@ -35,13 +35,12 @@ import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.Scroller; -import androidx.core.content.res.ResourcesCompat; - import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import code.name.monkey.retromusic.BuildConfig; import code.name.monkey.retromusic.R; /** @@ -512,7 +511,9 @@ public class LrcView extends View { if (i > 0) { y += ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + mDividerHeight; } - mLrcPaint.setTypeface(ResourcesCompat.getFont(getContext(), R.font.sans)); + if (BuildConfig.DEBUG) { + //mLrcPaint.setTypeface(ResourcesCompat.getFont(getContext(), R.font.sans)); + } if (i == mCurrentLine) { mLrcPaint.setTextSize(mCurrentTextSize); mLrcPaint.setColor(mCurrentTextColor); diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt index 3155c84b2..872ac1d25 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt @@ -149,8 +149,13 @@ class RealSongRepository(private val context: Context) : SongRepository { selectionFinal = selectionFinal + " AND " + Media.DURATION + ">= " + (PreferenceUtil.filterLength * 1000) + val uri = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + Media.getContentUri(MediaStore.VOLUME_EXTERNAL) + } else { + Media.EXTERNAL_CONTENT_URI + } return context.contentResolver.query( - Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), + uri, baseProjection, selectionFinal, selectionValuesFinal, diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1986a05c5741dcf22cd692ad6633091597e8f50c..43bb965a936504f9bd3db60c9ad8ba31a4fdd5c6 100644 GIT binary patch delta 3516 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 KMNUMnLSTZT>chnV diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 64bcc529a2ff2f097d929499800fb0029fb20455..a1e8f4e0326469e8b50d6ab9e2803a5be9370c3d 100644 GIT binary patch literal 2256 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 MO?{1Om|fKW0rr77lK=n! diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 1986a05c5741dcf22cd692ad6633091597e8f50c..43bb965a936504f9bd3db60c9ad8ba31a4fdd5c6 100644 GIT binary patch delta 3516 zcmV;t4MXziC%qexBYzEqNklR;}w@Wz1$>6K_|=@~SPx12qzm8<9a!90Y;s$J_t! zd2i;;z#ucz!{K(-Up+A0uiyLTKVQG@H{NK%jZ$jI#NLF+-+#aH|FW?GJAgRi-i-5f z=e%~nm?q%h>`i>%8l(eAHxLgd-Pr$HTheAr0VDtFM*n*k#1|wQWIxCikPMLGnwpw& zkSZqlxtM)-g&n(^o%^m)n`UWaa{%KgngM+71hNq1XAl((oT3my%o2VcwgGaKwb{wY zd>xy+#@Gvgdw+xc1LUa@6e6*WeI#tFF%rVQlZ4}IL}1u9yN0)s`8hN@tr0%^fSkqN z3s5A11hrB)RmlhzUel+pJ~g!iwr8K+Kz{DHRZB#h0e;c426EsHCh*0Vj-hT(?_l{#98x@iO#}gzQJHk3e zni`IgpW>jhtE#F7Gzui{_%s?#AgZwdhc-tM`d0bk#fu=DI&V%NE;}Y9eX@pHQi8lzEGrlFk5h02RK(nb z9LE`*9e?vVEe?(`r1J9eoRpN5cH$VV0tD?IYwix&T*qBfQgRZ8W@j-Vu71_k)hk{X zID%-v)M##Q?jI~NHD2t!eEG5mz^UN5#cKpePPfa-$|`Q%y46b@vsr*ZYksgsd#bBJ zm7ft>TvA&~P>Kb4Vkl2hiUd*V<(VjjguX1Z%zxA{^cNNu{)?HKgIT5p+B1fQbc<`M z?FR@QJ42#AQzhy*MWX(bBpNV5qT@p(8Wbea-~fq+`b%`8k3_@9N_6rlLc>Q8I`#L2 zP9I9>jKPG?`jF7sUW7*WCv~892<@k$`E?< z=FKs$^hI37Dl04hu0+nWIf2Mf0z$QBrGpcohcj_(ZS6txiU9MP$+gE5%nJea0YJUL zyxaj$4@Ce`Hd#q?a&q?9Yi3-%K;1gRiN#7pt}R=L>}zCy!?Xs?3gJu;;7lH^_kTS) z?HQysFIju~Yqh5?|6?s69?_mYeOhwhz=5tNlSfWYtyV8!+GBNCkKasaNC1O^;-~jH zVH}R&m}TF?Ov_k%=5g)uFw`Ce0LhRbC{YOcpPAGmW_xyEUjJ6Aq}aNVXf?%G4<$^{ z8uU4}G)I6l$5Ze6$#x+ypc0g`hfjArN0pASVA7cN|ItmS{eQLR?8V&AZhP=B9rh+g2N z!&&%7C_D+&f#F&+K%WrG5vu`klmdjav{;|rzJ2>^mJ>M|YmhxS=>1C0_iThb$ZxVX zNeuC4;rk;k6j5VjFoM=_hm85otKcXFh==>s)YKmg{E)E3!~s0{B_#oE+Di0EVyHHX zAs7*gtT7@Ot~DNYJ7ik|LVxSY&d$CDEL!QfaA)M>31~|k(bZkS0e^;B^i*$2hJ#RK zA7cR+TL6MUrA|ml=u->O@#DvP1E30P3)EVf^f)~jIg=KWMvP?Wge?W5FrXR+r~&|a z)dF<%=+Qn(14_V5nw1w5jI(K0y<=2c;MTkV#l^)9tOe-c!GrD^jYh4ciEU5NX3`VK z8kZSbIN>a5#O(PC(0`^skF`uTPA9awQW+B~E-o&MiHZ3@1SChX*w|Q?>gsAmo%G#X zgnlwx4~3T+;YKwW=0|Ji5PI(lp^wtcm!}2F%~PY3Ubbvm4>5|#fI4*O&=&F_MZ-MG zZ}$oPG(r!BYt2+%gR!g{^P}~j5L%*YvfDv2puD`iOlN23_J0O|7^L4pCVg4yJEwTv zUvCq-Vy3O~tgdwEqGNXM;7`EUkV^rBWTV)ve`}>)=eM z7=qzthe62!4tMKv(4@k~2ZW_Ya&q!Nk%hsO2P@24K^?NzmbaI49um4T9H59`1`1^d z%dRoAjs>$Uet*=ckR+TmJAL}}Us0i50 zT>!FmA(&fsqi7GhANu_HbGm)|_8G{+c)9~^&3^Xm*{(pbz}9zC(*RO*m>$gBex|GV z@8*Lhv_M9`RC!o-+~f^};Y zp^r0d0|?v-!P&$^hYo#-3;>i+zNK>)6>%xnOTg3QX?o-qF=6- z=%f64%i zHTF-07Ckn7Y{o4uq=poy2_Gv133Ef#o|2Lhx_@@<+G#lUI{lBhEnmJIpV8`^laq7K zS}oeTf#?cUEKXxSg#@t3^zm3%eG}ZUxgpdfRhIS68TZ$(U%v!nc41?$e`5sOCMG6M zudJ-JN{hC|3d>obyqF1D^ycNJO9?c$++V?OO=y_8ZG?QPAl*9cf$E*ETeog5j(O+K zoqr8m`5F}!^(O4zzkmP!Q4D{UE};7U8+IpSh&FS^6sqeEfH*5n(g}$imtXZ>0;S?m(BC{jfjU*Vr8c6m4NaAYvyb7SL zB=poSLN%KD&&A|$51g~}GbW!i5&wq$FMr<=T2W?M z&E=6NYt~4kySRJzZl;Ti%U~Sak|j&5dn5rGXal=W^XAP9g*r`*w%bx4MZmsiW@eT_ zwJ-_C#Kva*HfPQpXV`U(jg4LK~@ z5_ibxbHJ?BYu2nm&2jhk_QnZ$n?2}j@9*!AZ$5B~jEoF>@Zdov%eIMyK5508h(wS$^o>9T%YMKxXUVmFQDeTj^ zbLaj8eH?~;Wqoe6N5t*MjT?tg6L$ge+_h_0Y<6~bDgI(j0ZIgeeSnmtbjOYz-$LK; zVcssRuZ{lJ7`wrP2e)x`b;U)Gy?uRs1J9m4d#tFaNPhH@fs$-#di>QDEUMyNQM@a0 z+{ecUAK>kceZxLB1srj69Dg=!7}|Dskp7Ui&%b>6@C+`x&oK1xMLrhgVvbZ9DjxnAMVV|4ng&Y z%{MVI@vEeyq|@o?>G?Q&hZE$iwxh4wFihjI<>lqLG=)BV_%I*ps8iS`w#_ug9j}Sk z#y+%!*9bXa5$NgZi5k@n1XpYgfM0j?s#U8N?b)+u)0s18eo9VGz6Pm68aUO0M~@zr zfT=u3!O!6I3sO^4(|_<7@NGZs-Me=ao{McTZNWCX<26{p*>Ya8B&GyCd-iON2_`c? zT>Iw%(ifwjlatdRkfAu}5hF&7_w(}$M#0bcKlpA49)riSbMV}**cP^lZ8v7? ziB|Y)V>avM<@H|2jvYJWTrdNNPTrMCclJLQJO+=&bMRbj<27}HQm!~Gv5D*>-ZxA?mN0000P)Y_h{GU@iHz5y#aW^DhTXDuFxKyq@pxg53{$6!wI) zN9AXC89C>#1&sZ_?(*3mV4kokuyEKBSQ@PO>eZ`NFpD&PUo5{nBKQ4R9{UHEb2dti zw*#1#dN;uP2Vrl*cEbwLabmOCY>hJQI6em~N}lsU7x!&>yDO%-?(ZJ3uVI&6pxA`m z%7qxb-?HLPvt!G>xiofyLMKsH;u4 zIsiA9@7uv5^o>rz%4_IBaU0sXw(nNJX{CGrcUZLqbM+>GsSlL>n%KNIEzjV42(y*(lqshT{^aG*Foqi{+V{0!a&e zilX?kjCFA6b~Ihv%$1du18!*W*68D+FSC>Dg0TA?QEX%`vr$!%jVcRmWGbAVtYGO4%mp_!okX`U^TVL(q5AV0b+|N6@ja1YL+n z$Fe0bVH^^x9qp&W!oopK3Q3bB{Fq~o*ay9I*M4;WXL1_}KzXJlkN*Cg(yt8rR$u#+*z5FAsn zjWVJH9r-}efj0mLVDZFo3cLP<8{#EP{eRSTVN6RZtrA4 zxhHI7H5-RiRaKRhoSb~GzK?DJ!g-?&2V}TKd8Um{E*5m~ZNPzQ_WxB-0;>jKVxf*` zfY}2mQGl`&P__fgHU=_G(AJj#XQ-fWU>k=B3LlKm#t1sGNsz^4JaCtkl*FNH%?tz5 z7-**R2@ktx~fd6{Zn}9POZ~!J@j0z_PV4?wL55VjKm>qz!9Zlf~u|Qx*oGASY@;6$ zbPwv`08HHII^aMxJ0zS)fY~bHYyyal01`G((1rm30}Fwzg{{M9i3;{~j5{{-GhO-%Pz8S2- z2_K-s`I@2l0uF4Aw={fqXq7PJYMlD>^YeE|S8LHI*MfTb%9P&dF_ops;XJ*11S9k1-~e0m6xCuo-lwqpD57j#EfO&N>NaEvp7_Rs+meurJv>a10Va=_O-Df61X9SJzta zE8`BU2d=dSa8^n61nJ;>(FglNtz`v`R}sgetgNi?Yr2^xFUaiy99V2LbDdn|a4o%h z^sZ&EcScC{yrilpLb{fddI08YUuV|}mT`yG6Vy)y6bLv=dgB$}pG!2B(Jo)UT(WWF z#z*Qc9yN01<>hZk^)woexzt7n-vS&~&sZHC&9yZ3j8Fq}je3Ttfq9c&J-)To6RfGn zLsd_ZRL@etS=>v|!k$p-LSbln1WAdI^53mjMbh`0!M%1Fb+3YBGNp519wS`~fTE>( zIHgB5z_ql%9O9y$)vO*5N8HiW!*CV>&Vn8QI7%2E@neh@3kJ0({k7XP-+%x8ec1Ps zQTIxZQUh~MLHMe3L0D7s=+k-~ftlfi`bhP7JJhpMi#xj5vlM_nm+A@V4(nxT2x50L zjPwfv0|VRWnK)%H#2-&>UfIa7;-4YT>aoLl#ZeGOxeh0;5tu_<0yC#|y?U1SQv>tj z-YTH^P|sWd^6x6BD68?q53Y$gn1LZ<1@kIvy&%J z&XzS%OIHOoM}Q7DI^UxJQ1S`@8mktC_ShpUW(KrN2S>|VT41&hMD=N12S-!SS1zul zsb>-3Ea>S_&#W$ja?To?3gL8r{`~o^PH{-L#H0lx`60s-VsNoD>m0EsdXz&wT43I$ zD+ukbB}eMvF6z{_4e)H6@2 zXC~mx1Sqqyc_Motx_I$o+S5-zeNRoG){3IAu&_U3rvjrH%s!_1hfC&S^|%y-PU_LY z+2AA9qYun3>RHBtxvwKIFOtDoQ_qY}fHy)IzEvbMS>co^Qyc}O1}H8r?r{Jra|cj0 z3PqkyW*-VXJwxkg}KqEpX87xhd7pq=yG1B66XzHHgD zr)vRd>vlB*DlRUr@b~xsFFhc877GXnXk)cn z3)}_7b|KED-kH$Foh{lVn7-!W!0hcjOXw7sB_M6IUK5yITx-hn2+qiwC3!Zt0dbIi z=bd*R*Jm+1p!@H?{~pvqZl(E}QGGdLQGi1=HA4wM=ZabHTEYEYTuYzUdpo<XdXH`P7&QjCZ8OmGg!nsPSiY90-a zevbK?6*I$uYkeZ!YAW7GEfJ(x-M@#=gxU9{<;aXR1unM5Uy!My$(G9q zo-~V(kDo7x8np(iE%)x->t|$%?)8?o6r4j{^nrTYW)+~U1e6sF#mh1Au4SvGTq*Z) zsAgUdb(B5}aHdN*{x~LXrJ$;^CR-xI;fKn~%4pT9Rj)7$*Xa)SwdSwA_F7v|yx0_( ztk$ZIB0=A+w9ra#t9q-(Qw0*pDIB&$0|V78=q15)S5-5UQ+a3ARzQuv{9toC=F}CS zv$M1F2MrqZlpM`FKk^`n{VqR0e@|1!A?GO(LFe{YQrNgsTIN+nOZzFbxUULjA#6Ut z%!O(`V=&#+8fQ9eawjuQ?Nd%iwpqwrbwgI7+2hZ#W5*J};P+1Iafw5BA3b{X{iaCu z^*07_>5PeXP0Od{1M=u|&mvmTw~XfXGSO!}&Gcz^Gu)|~W^}2bNiP)ByB%}stq$q* zX2(=o{=fN@n`&wF9n=P_;)p(Ar?*}zPi?|Fef?UPC~dZf_D-F~5uI-jTaUr3>MpG%|v?wCOnpUI{P9r9?p zPZ9l`Xl^nW)7&dJH@Dc^+nX0wwW{}tqKJryJMnT~YHDhDQ@9z=GFqzySu54|uHRd$ z1*ILRrrrN@g*FT?rnP=W^rd$JRFzNj`dp&fJucCV?)fyWYXN=Sxrjb`p@iOl?g~xm zT}ClqTBzc3(_}A($^W7ND4(5Z5PT=zz#EOf8icTq1@S{TcKPXgq zOrf$gQ417jx#ARszMXEOts^Tbd{_mA`c=@H0Tsx2mGq@oB?Wp`(vp5ATG+=#bK#1g zcCQ9JGlfr9s3=<)b~&yMSv@5sB{Xy9%$K?Eb;ck4_TGE%@iSTvWo2dUH=-h|Nl@__ zh4KzsDer(nxrwlSuy|M;U>y?n8;o_$C7lYl(vH{6w0XFh!iJbBWRRKG_?T&hx0!;x z%*b|T`n->s=E+z!vx}8Je@USedxfC|o?732`st@b=yMy{_iJyG;5|V>K_ks(bK_LR zE6UC?92kT77M1`*2XbNsW$%GS2|FaEx-Pgj%19Ix`>vI?jIaQbg}xqSp|w- zy<|b=v(TbG7C^Ef(^=?~E((2$Y`7P7QCV^0)q~MH&6zXjb?$R;aB#y`zK$P1{x^8} z)7i6Uqd5IFbOBX$si52>6;2+*i5GS_xf+~Zg0i-&R-Rt})I{M~L5VW}X}FcbhFYN{ zE3HQTw8G0u%RH=bH!I2lD{3Vx&F-dP(5lcBSm-3Ua>FO=6&ZW@Zt9<8pBuIzP;o4} zXY}aNy?F{!s_42I?MjNEy!`;OPuSt)#KB@z-)tU6_mLdDoVQc+zsCyJsu(` zaLljy!P@&a63aW1f#ifFZTLB~mkf0zynhrpdo>yolzRx+|u*`)pQa*I(P*3iw zzrTOu)+youCi>9Mojd(`6&4_g>(w{K$pDi8An~e8-hdX9uWTaj1k*9K`+#Dz)d$%s$L^|MPN;v6oo#G!GH$7^)Vxu2%+{Tv` zlmH5Xwt{%1Ur4HL8N-Npol8Z)w!rjo*5U6>f=rhhpO!f;{et-R&FcyU15ywm zeE~>|;A-=FDm0tjtdl}>;EI>hYo~N>dwP0$1x5=mai3)0+~3BG8Pggsj|2n+yirtC zWV;rmiflC~*MbuY1Dp(~pfuU7$#p6?FDQB%;*l?04MBMcTrB`@#u;rY)U*2wL6+*; za4?*(p&*=yzC0@X)MP(W&}`VSVZX=g^F zn-Y$;HSgWKH=npYV$)kWIXPu=BwQO3XP_!{A`5Ccjr4y~P(>PCq_k-Vy!=2*4r0v- zxb%;Rb7_cc)fLVGnPU#zs(kwN>8zagJ$m%u3HclLpl|aRUwn~&`Jmm{v15mxJ9jQ! zR@=54H9cZ1F2!xyzkmO?Xk%Ayt8DX4dqlih|Ni~?Y2t@r9YR7v0x~l*OZh)`D?sVN za2rsPl&)UAdLi1z5A!}O+j`S~HKtjoPMz*<+qNw)dVI>$)6;j~zJ1Y!g@yJ>PMkP#Y;0_7 zL|R%}KHpB!R3(49Lg;_v1mD?Y$7@XYnSEbAMmwI*RbF0Bd~UwR+i$?XyesMxV z!uI3Gj~_*;kbwJ!-mCtET2QFh0pmIUqcQ! ze?334q%R4ceDcY=xWJU|$7}zdfc*z&zgDeUJrC=`o$lGQr;nGH*FZLY&!6GD&b$xr zE05u0AK`QHIr-c-Gj-o^Sa69u`M=H8?6c24`^N_!c;F$P3zpzG$hVdDnEb2_@5B4@ zF?=ka<6jBNH3K_$6e+nSf@-UQ0|(y4OOe_5J%7gENf@^*;Qk*=NfzTS=VNmK0000< KMNUMnLSTZT>chnV diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 08d7ea8a36b79e2d66b91c9dfb06d4042b287304..a8008b182ec74ecfe09001526c3e455d35e63ac8 100644 GIT binary patch delta 2194 zcmV;D2yOSC7o8E1BYy~FNklqN9=iGaDd-Uk!1psbnz0f@9Rewc$V_SN*$Md>y@SbSy zHohBx=85LbjqV57XzNaWJy+oPek9r^wBrVY;aA33C0ZTcH;~aieMa9=8{T$gw-ceM zxA1Z}g1Um%ga!ywwee@JP#pa|jtM>+ zZmt5~OL0F1L4W5dfD<4+lP=I`9iGMac{GN#v0VXv7XlX)g9}c8wradD#-&B~yE;um z`5<=qt?YH~HG`|bTS!nZzi)4EKkhUMWj4UhQaE^7ztELtnhXYQZEa@|f}181%HYAy zz9aC;PP@sB&T49EI-wxpriOq!_xw)T^W6;INCL-adVjqh8X6itQ(|+N=TU!?<$&!D z+1Qg4IT58K+`D&gy4^aV)IGA!Qv|Hd0rYwXEv*b%tyzl=Yc{vGR)-`+42_MAzazAs zmRLJT2fe>lAsw69hJeR^BfzZX0?ZB-U~Y&2^A`xPAW(osvjte}FF@!F1|Lpk5H^Xy zs__igynn+Wd<=sPBN=QS#$byVgKeW3+^evkok&WE3vd>;Ta}W`2w(f5_sbQz*=EPh z=)JhFP2dBA1qccfVBuUO5Fo%3B(U^-2Fs@~Sn-}jU@Z|qz#ECcP$cj+gOBjFrp$4j zAgAIiRHUb;59}}tGNY$YpN1>E4~__Y`IQ8mJbxcFpM#gZKa;_-Y3TjQ3|3BLu=-sF z>&7xz?}G$}Gl=k3Pkgs-0#Dbf#Ks3_N-A`Sa&}j0AK#-3YA& zT980uoRmTdxG{}l^<9Ahy#=MTbDU%Q9J5yKZS4e-laqao1k%#d-oheQr-cAEgB%?O z>mmtws4|se4cIz|LFPFI|NLd&YJYCwg04jy0ZjQ1jvYHT%t#2I1t4|8iQSvtykdva~V_>YL}CYsl2>g zhge@S+X~2WaP8W)9i%ZeGNM9u7%TD0r2=GJ?)d$U31a-kYPXb$F$|V`fR%S4&cu&&nr1GiqJOIlKAm8l1j(s1 zTm8`^rBBR0m41GHURF2uoY4!JnVHda79;MY?aV^+GX_WY@?96E&#nHF{w>@6cF8u0 zz4*-C634--@cse9X!U$Z!N~Le{rd%uVu3uYL#?0KkG7A&$!M(1m#nwFmB%1Cl0niQ zbED77uB+Dm4aD{a0e?Jq?p!Z>r*eIc9z7b08~iqYtY}+Gf)uo}*#)AvZuX0$UZBkL z=+UFL*x1-G!iEb^4>d=NFQ=!cAEKN~)Au}1=XlyQ;6@8BT)2=xm>eE0%Bd+VER3e; zwcOm?6xxI0#k!LO2hYfXnCnvs3t>|Is|}9_nkw)7{QOINCV#{!?P~C{Y-r4!oSe&u zVJKm-se1=MaP&bNmX(!t23M;h2?{F8YF*%U@dpkXgJgr*kUT$}Fo+e`nTtP2B)mw6 zSR-2b@r=P$L=?cqz8*e&*qWM}nn+`I3cONIzP`S_Nr$*^SY1|DRzR~s8A?HfXq@*0gzQ@oQG**}V4T0xilGLk%2M->9>eQ*|n>TOj>g($p$%9nE`3V|W zUh_eOrlu0*mq%@=Ej@$J`tUybQlBv~G0){c6GUbp2SR~2+SAi>Vn9HEe`siE zAQ|1$XY?I!GlW0m(d~csur?!eI(2%)gNQs}oqu7T{OQL1_Ivr+PEYDU z=TE`9v~#T+tOx8RX?)(ToqM}*TTc`?zP|$d0JgKax%q2VRr6qFIIm*kbN-F5@jde1 zSH5vac<9@(>W=ZfZ5+zw zHjS8&qv7$mz<+l(oeu=)OcQVhq^SBq9kd6Y#rI6^!%^Rl1N>(IE=-+^hXC!+WLv6~ z7N39i$WE9aM1$K6)+KArj|+So2`a{So6WZKp`9>i12h&U!5jK*pGiffh6C7k2(7~Ss_*j5H3V<;He;ABMFn9nz@EbuP<9`J0h3y^-!}otj2(mRfbwa~XU0r=1 z(4K0G^#SRC{Xd(e({8pYNm428Bc1s(0Dnq?kDIK^<_IJc4e*BmJ{-&+1nZ#;o`L^b z(5}&fwvQ6D%^Pw`cAlMBO29=Jg-;wx$?yoD&w~A2Q*L(HXnmS2q z)z$f$*l)MN!c&4$!2UmwX%ePGI#YG*x}g}Yxx&@_pkXR2D?4k71rPfKaqr%}mvNKDNMY?YWUW!@>sc-ApJes{ zK6a8RaN9^HgxU9QVSg*YZyBkxzhRi5fFbyVtQ4E`v|FIdvk(|g+|HzO@UUw^Zt ze4LgEmAR@GxQV8e83*vkB=}Z=3+aT8Z5_C+*dHNiEgbM4EXZ%5pvn@LGHPZS<>cgi zY)`4i2-Mcr?sk>;+3sCX=`7ekJzWo6md^3E(t#ZW`}@u8gW(`AoqYzsX@q1S@>w}Z z(DDI-O0K&uGhmdTpC7`rv$?$RsDH1oPjkzGiz=O&*`obc(gE;D2LHC6^ELLpO=;5< zxL*yI><`h|U(#PtX`Wkk5|x&gW&m?bDosvK?$+4YnCFfIwS_7rOw+Zm1+F0-BkN4i z1DCT-unGK5DV@zyI_o6+t013cV1F?jsIPYI0L#3*y!@uWzyAw%2QFT`_Fo9LNmVd^+Dd&e6(gFKBIc<9B;5l&D57)tKnFrt(fqg$XdFz7O zAqPT2LVDXBh>MH+2_~^JcOBpg@}K@KS%<+J#hMYg%zjWifx8CM`BDb%{MXS2)(|$U z`wk%G-wO;3>}7Xg!GZ-ZG&D5ix$6L1#SN8WKhS||%uDbbxVvCMTz~J3Y6;v;QaWp7 z+FT0o3jlsCVyGlCbHD{bbx@;E0Hh*Fwyne33yBFF-cIC>IgTU;mVV9+Z$K}hHKQZd>j_x{C-p0J} zMH6iuqtGS*{|dm@8)X7ZhI2hMzX-rrz`3{$DplR>_^t&Xh*e5VOq?rc;+9e(G&Hm? z=7Jhm#aw&VXc3fopoVr$s-$%zE#yC}p8SS1($awqn563H^M8IdH0PJ4w0TY$mEE>I z%-)e!%t7k40RaJnF`_~h0iLbO=C-F<;WshJoq;K zy?;La_4S+d@vjPKPM;E5`+gM_T(xQ2Fdc9}!<3twn*yxgX|)tE;vh9O^)IY3cVyI5 z$7O|}s$4-e`F~iP*S1`%xua0(+Ik9~TuZyhSJRd;RkVI&75NXZ!Kheh@xVG-<=sf> zkuFPU1AH?uHly63qeqXje7dw+RvOE*{{8#^7(;Z!)snKI0;w-cp^{{UiqFCl6uOfn zw7ns_q^bqw9I??6Un}jOXr(>lth8;6l{R}>DPV+^{C@_+M&SBtK^4WWw;nltBKzd^ z>C>mD!-c){=vzriNsD+dMq5cc8U;w^?!N#&3BVHt6`vMVbVAVWW2h$Kgsraiy#=WV z+SxYRKfy-3#@T4wXd8muMr(%KXvH8K;zOYwe-u=Bwf*2VvA>KlI@}*JdDP?1ojcj> z#X@5x>VNjL0GUa3@2k+>xe7IyMi5F6oOvoM zE1OoVSTTjMq44x{XKV4h@$vDiIOp>4nI|_}gQ_vl;BUvk4oUE#IFAt2SmvY@N5B?j zp!WF?vhn6QLmve{jKX}3!e;zlbH7#EY{o*1Vq#(f7?aDbMI$v$nKFfk=m+P{oeSeN zD1VMtJJVJFk<3Tx;0r?Fz+OT3&m)5=uG4Nc*1m~;C^yC{v;k6DIYhxEr_ibGdRCI) zRVEO*K9aF8Cb!?($avtPdMPU_D^`w#Hl!Mps2rGXoowj15e>-Cjb zSHSr_QxOwB3Pmr2{3=^y!+mCCWSjtopMNrzc6IOK1xHs{ue7wZa8#?B6C6~W)wE#- zTQ-aS`LHiG8u2_YCAm#*oBLqdAR99CKVuBqjQcQ$e2|^!$vVUwVU^o6&1Od)@%54c zTx+YMqQV*(8M&7Gd?fH@IrZ+{`)Sr8)(z9IUAvaeqrn-<G)sR8Gt=q`u$1d-uM3_wHSE>C&Z?^78U(W{_uaxr0V+591{v-@~&i zq*9F%K84%hwzy5V^8Di%4cW;vuzwd}Kc6;j+FK~cm!KGogbXsdX2G(r6cbPd8=n^y z73D#GnS2l5%g^B1S7jT|aGT4QFMlHcnP6w0mq0l1UhC1LM?WtwuaQ1JKHhA6&cE?B zdC!aT8P9M0&mN9Ogfqp}t5=_vabk=P|Nr6J(w>vQc}jhI@-vS^=l=uWp%PvXLgGWe P00000NkvXXu0mjfLR`zq diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index cb23a30b9a9e91a78f2805104b2bdddc5c01eda8..3b2d6ffa530229fbf3f5de8b17393dad63ab45bc 100644 GIT binary patch delta 1376 zcmY+EdsGqz6voTK9Beid5uf=MpIDh$%(PMrQ3(aX@)2iHnQdyrO4G2EoX;8M`~64@ z$gr$bFdW8oxEc6BloFiwFts)no#JD*zwDmx+;i^l-0$B1KDyB?Fwc?(IEeB-g1a&@ zQ+g@}hlk!=vJILzk%{wqAgXrp@K3*#sFC@E>StH=mHwIroYyBd%&;+KxD7y?3^SmB z0nqrEAUac@-j+ZDE<2kH!*D|m{NQ{nrfrQaUa zGEdvwXq)E=Pry!dFp~6zLKs4B#|%K(TMm@YA~GHp=%lN}1;AzpzU0ZLzaDwDz*CcWOAywADnD?yv30Z1z@q?_np$hj~*{!9}1) zXe6;xOU`m>@de2N#(M=7x+Fx3jwRbn;I?fXDf{PVPO#DSyJ~PqKF485nQ?wMmF&|u z*nhu&u#rxows{xTMK?eevI7ZOkcB$S3p5twskOz0a>&yHL6EKuLj&s;n#<_jX0CF`ZXgOvMsKC@; zwdS=Aw5(YJFzRAs3l&Q|N*Zc*x%GYFao{eB_ppZpG*u^b^hl=(-DklWh@K?EYsI=` zt8Y%C5QRR&xQzv?=$=Q8i&b$JNC^F|nZNU&OWKw&i>P}-9ZE0--rWlgGiRAcKJI|!6#v8R5}b}V~%1$lffaZ^CG{@ zfuM--YIxs8*>U>}K_9#RP$nUG^}(9DaQZBGAw^5~NQIRp6Mhm?EIp>Z8ys?$#9!C+ zL?Ib^`#HXJdwY43@le+);nDBXdGxa9Tf%mxu1yjZQt#`iu-2YZsf_vi@dT3KRW{s? z7J@^!{A(haV*LaT*cddi;oR%JK`z8D*a%eGp&t_oRQ_N_?X5T!VW(Cb#em zjNK?m10`aNE{(QdV7`iwNb%!29m2)`IefmwXxJ2@=c)?0rEw)SaD@B+W#Qs#yGU<8 zgrHF>`Y)nH9pGszg7Nm@*PfkH&}c-uD-i8llr%TwrV3i|`^7!E zO|vi17HvomCGpcLPt+AKMq?KSXZxLJQIkR2PBj}kNIJ6UAo;LVz17w?A_KdPhS?szZojV0?|o3xtH~c zVZ_i@Q91NjWfOGWKytSL#(o>nmAZwFg>I5;-5>g8Xl>4{wC`3{KnL-=^llLHI9oOf NAkl;kp>+%lKBt#mQuk}09F&pWru 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 PSBLDZoX~Vjf8zfD{IY#( diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 08d7ea8a36b79e2d66b91c9dfb06d4042b287304..a8008b182ec74ecfe09001526c3e455d35e63ac8 100644 GIT binary patch delta 2194 zcmV;D2yOSC7o8E1BYy~FNklqN9=iGaDd-Uk!1psbnz0f@9Rewc$V_SN*$Md>y@SbSy zHohBx=85LbjqV57XzNaWJy+oPek9r^wBrVY;aA33C0ZTcH;~aieMa9=8{T$gw-ceM zxA1Z}g1Um%ga!ywwee@JP#pa|jtM>+ zZmt5~OL0F1L4W5dfD<4+lP=I`9iGMac{GN#v0VXv7XlX)g9}c8wradD#-&B~yE;um z`5<=qt?YH~HG`|bTS!nZzi)4EKkhUMWj4UhQaE^7ztELtnhXYQZEa@|f}181%HYAy zz9aC;PP@sB&T49EI-wxpriOq!_xw)T^W6;INCL-adVjqh8X6itQ(|+N=TU!?<$&!D z+1Qg4IT58K+`D&gy4^aV)IGA!Qv|Hd0rYwXEv*b%tyzl=Yc{vGR)-`+42_MAzazAs zmRLJT2fe>lAsw69hJeR^BfzZX0?ZB-U~Y&2^A`xPAW(osvjte}FF@!F1|Lpk5H^Xy zs__igynn+Wd<=sPBN=QS#$byVgKeW3+^evkok&WE3vd>;Ta}W`2w(f5_sbQz*=EPh z=)JhFP2dBA1qccfVBuUO5Fo%3B(U^-2Fs@~Sn-}jU@Z|qz#ECcP$cj+gOBjFrp$4j zAgAIiRHUb;59}}tGNY$YpN1>E4~__Y`IQ8mJbxcFpM#gZKa;_-Y3TjQ3|3BLu=-sF z>&7xz?}G$}Gl=k3Pkgs-0#Dbf#Ks3_N-A`Sa&}j0AK#-3YA& zT980uoRmTdxG{}l^<9Ahy#=MTbDU%Q9J5yKZS4e-laqao1k%#d-oheQr-cAEgB%?O z>mmtws4|se4cIz|LFPFI|NLd&YJYCwg04jy0ZjQ1jvYHT%t#2I1t4|8iQSvtykdva~V_>YL}CYsl2>g zhge@S+X~2WaP8W)9i%ZeGNM9u7%TD0r2=GJ?)d$U31a-kYPXb$F$|V`fR%S4&cu&&nr1GiqJOIlKAm8l1j(s1 zTm8`^rBBR0m41GHURF2uoY4!JnVHda79;MY?aV^+GX_WY@?96E&#nHF{w>@6cF8u0 zz4*-C634--@cse9X!U$Z!N~Le{rd%uVu3uYL#?0KkG7A&$!M(1m#nwFmB%1Cl0niQ zbED77uB+Dm4aD{a0e?Jq?p!Z>r*eIc9z7b08~iqYtY}+Gf)uo}*#)AvZuX0$UZBkL z=+UFL*x1-G!iEb^4>d=NFQ=!cAEKN~)Au}1=XlyQ;6@8BT)2=xm>eE0%Bd+VER3e; zwcOm?6xxI0#k!LO2hYfXnCnvs3t>|Is|}9_nkw)7{QOINCV#{!?P~C{Y-r4!oSe&u zVJKm-se1=MaP&bNmX(!t23M;h2?{F8YF*%U@dpkXgJgr*kUT$}Fo+e`nTtP2B)mw6 zSR-2b@r=P$L=?cqz8*e&*qWM}nn+`I3cONIzP`S_Nr$*^SY1|DRzR~s8A?HfXq@*0gzQ@oQG**}V4T0xilGLk%2M->9>eQ*|n>TOj>g($p$%9nE`3V|W zUh_eOrlu0*mq%@=Ej@$J`tUybQlBv~G0){c6GUbp2SR~2+SAi>Vn9HEe`siE zAQ|1$XY?I!GlW0m(d~csur?!eI(2%)gNQs}oqu7T{OQL1_Ivr+PEYDU z=TE`9v~#T+tOx8RX?)(ToqM}*TTc`?zP|$d0JgKax%q2VRr6qFIIm*kbN-F5@jde1 zSH5vac<9@(>W=ZfZ5+zw zHjS8&qv7$mz<+l(oeu=)OcQVhq^SBq9kd6Y#rI6^!%^Rl1N>(IE=-+^hXC!+WLv6~ z7N39i$WE9aM1$K6)+KArj|+So2`a{So6WZKp`9>i12h&U!5jK*pGiffh6C7k2(7~Ss_*j5H3V<;He;ABMFn9nz@EbuP<9`J0h3y^-!}otj2(mRfbwa~XU0r=1 z(4K0G^#SRC{Xd(e({8pYNm428Bc1s(0Dnq?kDIK^<_IJc4e*BmJ{-&+1nZ#;o`L^b z(5}&fwvQ6D%^Pw`cAlMBO29=Jg-;wx$?yoD&w~A2Q*L(HXnmS2q z)z$f$*l)MN!c&4$!2UmwX%ePGI#YG*x}g}Yxx&@_pkXR2D?4k71rPfKaqr%}mvNKDNMY?YWUW!@>sc-ApJes{ zK6a8RaN9^HgxU9QVSg*YZyBkxzhRi5fFbyVtQ4E`v|FIdvk(|g+|HzO@UUw^Zt ze4LgEmAR@GxQV8e83*vkB=}Z=3+aT8Z5_C+*dHNiEgbM4EXZ%5pvn@LGHPZS<>cgi zY)`4i2-Mcr?sk>;+3sCX=`7ekJzWo6md^3E(t#ZW`}@u8gW(`AoqYzsX@q1S@>w}Z z(DDI-O0K&uGhmdTpC7`rv$?$RsDH1oPjkzGiz=O&*`obc(gE;D2LHC6^ELLpO=;5< zxL*yI><`h|U(#PtX`Wkk5|x&gW&m?bDosvK?$+4YnCFfIwS_7rOw+Zm1+F0-BkN4i z1DCT-unGK5DV@zyI_o6+t013cV1F?jsIPYI0L#3*y!@uWzyAw%2QFT`_Fo9LNmVd^+Dd&e6(gFKBIc<9B;5l&D57)tKnFrt(fqg$XdFz7O zAqPT2LVDXBh>MH+2_~^JcOBpg@}K@KS%<+J#hMYg%zjWifx8CM`BDb%{MXS2)(|$U z`wk%G-wO;3>}7Xg!GZ-ZG&D5ix$6L1#SN8WKhS||%uDbbxVvCMTz~J3Y6;v;QaWp7 z+FT0o3jlsCVyGlCbHD{bbx@;E0Hh*Fwyne33yBFF-cIC>IgTU;mVV9+Z$K}hHKQZd>j_x{C-p0J} zMH6iuqtGS*{|dm@8)X7ZhI2hMzX-rrz`3{$DplR>_^t&Xh*e5VOq?rc;+9e(G&Hm? z=7Jhm#aw&VXc3fopoVr$s-$%zE#yC}p8SS1($awqn563H^M8IdH0PJ4w0TY$mEE>I z%-)e!%t7k40RaJnF`_~h0iLbO=C-F<;WshJoq;K zy?;La_4S+d@vjPKPM;E5`+gM_T(xQ2Fdc9}!<3twn*yxgX|)tE;vh9O^)IY3cVyI5 z$7O|}s$4-e`F~iP*S1`%xua0(+Ik9~TuZyhSJRd;RkVI&75NXZ!Kheh@xVG-<=sf> zkuFPU1AH?uHly63qeqXje7dw+RvOE*{{8#^7(;Z!)snKI0;w-cp^{{UiqFCl6uOfn zw7ns_q^bqw9I??6Un}jOXr(>lth8;6l{R}>DPV+^{C@_+M&SBtK^4WWw;nltBKzd^ z>C>mD!-c){=vzriNsD+dMq5cc8U;w^?!N#&3BVHt6`vMVbVAVWW2h$Kgsraiy#=WV z+SxYRKfy-3#@T4wXd8muMr(%KXvH8K;zOYwe-u=Bwf*2VvA>KlI@}*JdDP?1ojcj> z#X@5x>VNjL0GUa3@2k+>xe7IyMi5F6oOvoM zE1OoVSTTjMq44x{XKV4h@$vDiIOp>4nI|_}gQ_vl;BUvk4oUE#IFAt2SmvY@N5B?j zp!WF?vhn6QLmve{jKX}3!e;zlbH7#EY{o*1Vq#(f7?aDbMI$v$nKFfk=m+P{oeSeN zD1VMtJJVJFk<3Tx;0r?Fz+OT3&m)5=uG4Nc*1m~;C^yC{v;k6DIYhxEr_ibGdRCI) zRVEO*K9aF8Cb!?($avtPdMPU_D^`w#Hl!Mps2rGXoowj15e>-Cjb zSHSr_QxOwB3Pmr2{3=^y!+mCCWSjtopMNrzc6IOK1xHs{ue7wZa8#?B6C6~W)wE#- zTQ-aS`LHiG8u2_YCAm#*oBLqdAR99CKVuBqjQcQ$e2|^!$vVUwVU^o6&1Od)@%54c zTx+YMqQV*(8M&7Gd?fH@IrZ+{`)Sr8)(z9IUAvaeqrn-<G)sR8Gt=q`u$1d-uM3_wHSE>C&Z?^78U(W{_uaxr0V+591{v-@~&i zq*9F%K84%hwzy5V^8Di%4cW;vuzwd}Kc6;j+FK~cm!KGogbXsdX2G(r6cbPd8=n^y z73D#GnS2l5%g^B1S7jT|aGT4QFMlHcnP6w0mq0l1UhC1LM?WtwuaQ1JKHhA6&cE?B zdC!aT8P9M0&mN9Ogfqp}t5=_vabk=P|Nr6J(w>vQc}jhI@-vS^=l=uWp%PvXLgGWe P00000NkvXXu0mjfLR`zq diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 1a3f47f93077d62b162b3b0581f55403a8e1d52f..0fdcb0cb7d15ba50314b41bff47e231dcdaa970f 100644 GIT binary patch literal 5189 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⋙SF=K9;yC7#ruuID~hg6zyd}6()T5Nj_F9i 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�zUG1dKLH4mSI-Gl>;M1&07*qoM6N<$g86MZ0{{R3 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index d719d15758806da78be29543ebfdf7e477bcf990..6ce2b8c4add6a46b2e8e4f637f9647da62cd454e 100644 GIT binary patch literal 3273 zcmcJRc{o&UAIJ3?vKPjd>}w3BkzLklWMpkH%+QcETLv@PTE>!)T_UM$nZaNzMbR^5 zogpH|Wa~-F7EfuW5WVBM-ap>I-s}D6Ilt?i^SiEd-}mQxzUOzJ`@ZGkWG5_eOn{4v zOW6J_)QxkWJ~;V!IoDf?I$yZBg!1g6)`%=JC(%lr4QCU6wm^=I9`;!s;EP-dpOd9bU11ikH&m zFj;0b)gV$GBVsZ?QwR6W3Ry)v`WN*SrGbjm%TH7N)36aIh9z*;i-!l?edlA{LJ4q4Gii6Rfs`x!fp4>DeGE?|>?h9;>TTGS}^4I;ZW> z{4^!B)4Ld53_ukC!|ypa^AySrOEfI-;qZY}kvc|ayd3pCP7vp0L-cPce=UJ)I!nG{ zzFrRGAXj4bP|X!cFh9>sHBalnD-g9|qxi(#g8CO~C#J6gQea~1Z@o&_k@-mCYj?*V zki|!$!>#)?AgCa0mzzoWa+2V+s6?=~6_gYRjB9WKg@M+QC|`2JM*IAVFHtxLvc?(T zrvp>Lk|J2Y5-^N~si$RDW9?qLGzefDBj~<~ORK&cJ8S#pYkJ+(Xgpg)nPA;QxPNDy zFT_3&51MFysk05o7JDTw%~gyV$iM!I*8`-ai-%u9z#*M(acV~T*~m#+@%FrD&7AGl zKI7N7&1*|CM?{4(#lxZg`5z#2w2VN!POz;?J279Wz*IkLNI z;F)Vz>ry8~eC}^zmbT%MREyqfU0SWbf#%~)eITm0LXpMZ1Gt+nbLCDDZY?u0Xkbzj+*$g)g_^M~f!nrkQwq)8vA z0Iq2F45!0>+HZ#ra)@^^JLY$j%e(|@M^je}khLxr#3X-{#rLn_KJ~L@VaKq4e+C); z8c3kiskK(Wgcub`T0?!3TdBdFri<}Y-jR$q!_-R8Cox7cy zX;wVZm?WnicKkc8(eyvKKLSzCZlJ=xH}dyGrq)h+YBnKWCwkw?3CvBI^49*fTsYGn zq=b}lORXa9>LL?DbxS;WZo+JH6D%}A~Ku+ru0?o<51$sXsTuupB`Yp@KY zm;VT<3CSrF0YhaqVi$@0NXAl5 z%IWCO@XtzR&2M0ZLez)S)R)s8)#ujAmpj^!HDn1oE5uDVrn5psC;~{$Y0Cm|%#Uov zyx6;~3Z>9a79!Q?UQ~cDtqSBE#-w#ue zXy!a9!Ts6f4{;4qp)v$mg}G8v1a*D9sv~aG72%qoIve@enW<#rdS^hMwTEg_mOe$q zz$p59fx%-M{nkdQZwsxK`a4;~O$}cdIK+)A^!aLKZ~U~^MIDmz9AgCDW`o_CWEeRq zN}p=Mlx{hsm87m@$_B$sSg1uL%@xERr>ckLEuMI{$7BpdL5*7sS=WG=WmOgt^cqG29d=AJ?^^&rj_0rN| z%39ihL;R^WLvPC(qO7;G6n384HrKId#^Hv(tA9HpAP>0 zlD=Q7f53dgvWoQ-f}%jc?R|aDkk^vV-_DAAKEM1nf5uYxTeUJWGe!@!{f#}MiOUNq zdgfQjxSG@qD{#z6BIguzW9tm%q^L*3Y1w;iN#y4u?I+`9Ke^Oc6ktC+Yv0~oxL3E? zKQNlZSw}Rde_~7*$EwftmCCvvzkZ8r%+57uqi$~VuM#}e;=*J4MD7=Bu%=5heKX5^ zroQ)$XOC_UJg&t;&F6&}`2znF&!G_v&cR4_w5xBRWid2e?^wJ4ugt z?bQQaWe(>qtm(YPj>}|mQrzC9+daN7Gzd!4e_2Q?N)rxoLWz_7 k&-A~W{9`+IF0Ns~{*e^nv(U>PVzCDp`!h~ZvJHmtZ*^qG7XSbN literal 9034 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⋙SF=K9;yC7#ruuID~hg6zyd}6()T5Nj_F9i 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�zUG1dKLH4mSI-Gl>;M1&07*qoM6N<$g86MZ0{{R3 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 4f5dbff3968f5d2e36c6ee28fc7eed100d692abd..cd7cb3667a39bf0a6f7c3a039404e3e6a63d0cbb 100644 GIT binary patch literal 8281 zcmV-fAg14mP)8n&=37^)`KzT0#=5stPSJ8=c2b<>4XF!a3w2S5D@5`%Jb=Pl`vq89c#z$ zdV+a_)dy<=)(Wf*Su6Tkefk|wTaXA~=|DLdG-a&;13#}0)*fsa*etMb!F~k`2fGP& z4=fYR04B4?hfF`qq~EzofAcGyV-}qg&s{wipvvknISDlM%}1{>0_Ka{ZH}MOg zd!?d>$D(mb&`tw8uYd**4op$Pk{76qK+&~{mwTS>p}z7(8Q44cQkno;6DYTxhk8Hprg5yJOU6eP55(JGkGbn@-wtoceZvsat)Fv*vH&W3$!uw7%W>Qi^RY61B8AVttTiW4q6W-4e+_tl1RK=FL z;g?BhpE+u6Sn37@>Af%5?+UM@Lt|YZ@utEy^Hm$_DDhJe{JkVeufo5R1+Vi+hs1VR z6TDIK!-o%FQ)}DOpm!GOUZ-%pk-=bC3{C1%qIHpwWx2zRZx>Q?UF#GqQ!{4$Sy@>- zu3o)bu0*E#Tan!)?ONbu5LZJAeMXbA>aWMkXgGKMC(I>hL%YBFxSpafv*9_;7Wn zNb&hV*2Rk#F;zdVYaYiM|Ki@ifB)37Wy?G?J=iD=rMtj||ER;G9f%AD14~Fq*adx4 zM$uQA3td=>F<%(x(K}Ha5E-(vvRGVP+*0f#&4n_q@+CO_dzXcUg>}U~Jap)g ztD{0!8E}gy^w8y;p)58w_6+o)Td{{S9=hNn#yKiF0LMse2+p`u@;5hjFs8v>m)W~nZ#y(2DVgUpL{B@ zSxZbddokD|iOpGPvbhT+Hg7)IJc-Sp3pPh$3uc>a;ViIEB(`X##1_vm*^-ZCwq&}@ zKAi?O)nrSj$n3MpV3TCF>?4zX{vp^znf+^m%)USyC$r^fAIR*>u_jybzRAA&r_5IV z1MEGSef_Sbt%q}7J?)S}eRk*0o%?(C>}g04l_@UmQ$~`cwT=p13sy?3%WGh-f(?>b z*MSo2HUKO{V%=YsSdab^d;TSfz0eP=uf%%xkyx+Z5(^HNSnr--FG#G<^AhXZLt-y> zmsr295_<`)i^TdrC$X2Gm03uT%m#D<>nO8<&&ceR_A(pPPG+zEO=hpP1#2U-*8^oX z7;H#OnGFp9Yaz2?&15#bsmw++k=YxKW%gzxu!b@l*+6D*`N?dQugu=|0jqCm3qxdP z$S(d+1{#)KzI=HT_Gyu)^;|foq@=XxJ9s%lI8Rw<0;kI>1P=k!J;Vf04}j-+gy%(o z2LS5D0qSJ}s1E?tR|!xT0_a&wfCkzEsFfu^!_@%tR{&H$r_JmEq3oe!1a)F!q6|X1 z=mj;oIP^X{94VZqEHHtS!}AE<#|#j`(<@j35W(|ex5ohW?}G4vb+!cPl@0)p0-)Dh zo4k(*&@cjMgaV+E+6Blw%@Gw9wGYRJiY$zA?{okD{cgM<-ceb^)Oi9pLfA6G(~m+I z!Gjj8@IDsd3jyjPhA)YQaDce?5dq@fM+p#yujUGXc=!T<-r@lHXbT_`)Q=Jp5*T=& z7mLgb3EpQ9g)WCpS4^Gz*xQ)F5xkDs^N8A~7ik{~UuxdR2vBD`!&mE^;mZt=Qvdir z_C8t!2>TMfP-JA}FGb>oxc5m)N@}l#LBnZt2$+`ya0Ja`4qM#wD7B9Q9#!wt>9O}w z0`$6y{u#yrQid-(1hfVKGHRjt_;|K||Nd@;?uB^fm7Sfv#?jsfZTc+P>}ibFR{%%w zIy`KNp2rN2O6aoXeU#yAh)Vdf0H~pT`bPr*87IKg)6-c*L`30cWPIEQL{@D_jr%C= z<4+#zn_hyqu>eO!^BCb#>K+u%Lh2sU$%^(7#IsfJBZRLv@({kX01zJ+x^?ST^4hg) z{jtxtZrxh8utuHBq@|@z)JmzwjG6LdXo4xjB5*9kvLapQp2sq5$tHNf`wS35*T4?B zXdfE;84_T{`>2WMT*8+Y00M1`-77(o#Eu_7J`4N&^y$-Xg)!%a=4vn)PHQ22q0O8j zTX-3>y&!lTh1apBdCc&165z2Bx>Ut;Q|sR6EgRlPBOb`;9I*WL^5x6tpwG+G=(J0L zwNGMVVrRY^supJ4Kl%8vhvC3jK%?+DA~TB|u=CCCQW^urfdma%U<*2jO z&?S1GJiwzNfQ(@bOJBpn!d7A5=R1t?@b%SKUsZw=U(|wlHUl*EF(hU$L%qWDDhd@ZM_5E)ET9oQjzze#7P7=_tCgi-wtzu&?_dx zd9eP^3ZY5qX*yVH9IN02T8{CkjWSyZW+glrwrosSI14~Ds3<2UCbE6|_Kijs03i3g z>K`|VgUgJ09UNK$Xv%!cdLWz&L$6~7roAOFIiL|jm2z;(bI<+8G}4rmgfc>vD<%bK8`y-g6Aff1nL`o^4D<>GOy zc^$P94KqBx1%pS60HNkZ9RxDwDC!_&!Y&=e_0Q6!ORE7l(WHMg6~eF{NGvAnPGzh( zy@8?242(kCn7xe+k26frIu>4sm!}pW+j1g+jFYQZu3U*5I&^3qnxQXaZ}=)35fOop zyUx~{faYm@_x|byWx+iqqt~(UGGaQe@HRO@RWmE#h+Zcj<2=p=5I7H`kdBRwHLP2= zt_w1;di82Idj+)Xty{NV5jJwx)WW!FvxVY$0>@}n8-(*>2$2$+Ap$h!v|N#0i6YrT z>nLW{jb5ja;L#{R#_mTUy=~jJQOJZ{LYe?_PfALfLi$H@8-Y)sB|y_pQCQZ;q=#@` z4E2r$FiL1tG)@kWBY#N9l}kxU`AUldnirID zV9eEs%2L)~)BG5EAtf+5ps@muMTS*G;AmI_nX42-LqoqqChP)e)~s2mfjr<3cWD-& znK{Af+1qmY4lgXj(P|7|BLrwhs6dk&IHQUX94!Ncnb^aJ580_xr?A+*0>1nwF73as$3n4IeLsLxPXc-{vdkkgaZ~zmY#D+_5 z-Urh(SAT#1%Guf3k(vc)#$#a2;D|-^0zAAP8YfsWgpE{cn+BGn$VDBhoDCpk;KGFq z*J{?RiCYD`5+hdiPg(kC;2K9vTbHLOMyt(B)`g zoC%;CH*VY;J9cafWWyeS+&6FD+ywsiffff5jWyBJ1!xqZDhD{-a#kr0e8vQkR^ZWa z2;;NzX)|WbXlpk>@EQQ{WNH?mX{G{t^fY~J)I?j~?t!xH0*KsGxeXgO zv;=rEv31#12lK;+=lSSe`yty<4GX}FT&oyh-bb=FUc0x5byPqVp<Q>7*Y!==qn*Q!;k0)Nh`t)tAKL4zv6 zYqdZB{PS~X%EZ*{UZTvto^DFZbBJVHiwl*-_d*TzlrA!hI9ReXFx>KY`}XbYkOgAG zu16r0>DR9x(&lyW;K30k;aR8%(Xz1sNT79ahBvjbPE2KS;ne43cI9Nr3ujzF$HvC8 z<;$0kMHYw&y8v?O-Mcpm=_(T@OlWN|818C$PRwp${TB%9@c>GTJgiJr+qO=zYGX=g zNbwJs%sH@&7cV9c8#e4&WPt_|?b@Zx1;vt=mse$YUmM)m@6IZum6jAQv(-}o76mvX z8VhU28mf358%uuB6P^8~WC4Ww=g^@;xW!#PWWinmZ5Gl&K|!AITJzGSON&YhKxoO3 z;xBy%AZex^uHIBzGiJvk!$}=wcI~wDhciBan3R;nHgDd%29v1{9XeFDS3sL}P|uz{ zF%zry?YH0dLq+b4<3ZLe4m#@WVQw%daKD}aMaes z=XH7WG`Az!O0@`|@3Yq@jIU$6osoJW=FP=kr8j_}aK}<2Ts%9Xoc!-RfQQ3XrIS zYJC3r=e;2t)66Q3SqmROnfagNBzFBq{?C<`EVGR>pyL}9?h@w-ZDn?Hw{wQCsHmuH z$iQF2zR!>TG3y|#K=btUdSFT*S2>ZN!`}PQs zTYkcqIgB-L-W(Np4N&5N$;rv_S{PKE`LS&I5ZxXU>(^akll}=1-OB%YQ$d)2KdbN| z!tCtCPR(l~W1ohFg(W`o%rhOZ&l@*x?3v&2Wf8_&w{DGl!_*HC5B~gRqSvxM6P6V@9hfhY1U z{BVZjL|O}HTs%iaL}b7H_S*xnul@Y|s^mBO$`QtJ&qQBeUvGHzi;RriGq|>++q46bIP<<^#||u>_oKe{C}{Yy5YwQA zP)ygEJ9qBOsi~<3N2c_1Kg{_YFma`+agS*Dwl99)H2;;~&q+^_*}umYes(s#jm*wz zC!86*&-wG`r7>g1;K*~GCQX_YSUf8MLJ}*~uU{V*m-(MPdv-hS-svbWG=GAX=YO>` z)jx}m=3YD+!dXngAlmXt;d`R-aGl3@IMx$!?}LT&TeoiAk9|vh%!|hg1t2b_-Rsn; zgI=iK`|rQsJ1HqC-BDg>!8n!WI^v_bsqzr(e%=U>*{7o*O;0X>u)Zs^%`*$%6QL$L zxyPXp8NJURfBca#Y}l}Y*tfN7*GBK-UQqFD%?nj*)TmKyBsDZNbX`V9hC{s2q7SHG zUa`K5KPZk@9P%d%j%p6+>3<9MndzxATmKP2RLC1rEqz^PSs9M=KKO|54I4IK<^{Qu zKR#5YVquAA)s&tHt=A5Y=;-L%cp=3-I=yc*Kmh`JC_pp}02&4W4R39-5dhE|;C|Uxd3G*!m=-cFTRw8+ z$eqBzK-@g4UaeZSsuf1+o@+{vgc7L@wrtrl4b?k65xm%LKK+5rE}xaz*-)^XBM;2a~$bip|I%yKY1+~k^WtMi|aih-Hu3ckGmoA-;eOc7r$3hF?6u*au zM_qVr5)lz`m_nIB-wwrTX50<<#oxVq_bKSZrqq{Ji$eQY&B%Owd}`vD!8`A~^I}X) zOa@+_9?AWniaVG~^L(iAjO?|#= z+qP|?*k{ytUeuSx;(c-m>T1=hq5EqP5D?HYGBPq2>q&Hov2ug; z(08?pLpa+K)Kz#Wn>~B>NJ#BvT@Pt8eBv#n5ulWtGd zsZ+-v-QuQAn?A)YLFielkMz#d#7z66%aH}(znQ4L3iE^DF8vqFUmwP-0w0L0ouSly2| zpm(O@;K69Hb?eq6&?mUhb3+dg51fkfbY$pK^Fn242)rsB^)beR1s*+m^b8JI;lLH? zBHfB{un;q@ojZ4)gTBB=H8rNbsVYoYxICeeL)1fF-rnARI7;pB@89vni4#}w(a}0Y z#S729d-rg2%H5KW))4*d5B5xYRY6>^nYSpS$pW#Y?w6ru`7v&QktiuWm3zI(k?6c+A zrr5UB#@@~jj~YZ6hpU$_U*6Zt%d0uOZ#iz8XD1DLTGJ zMn>Wi+I3y&b|E^uTjzmR8F^umd5=$jW92{%|^vvjWAo)1fw{PD8*jCtP*ml^4)RrF9 zwq>3u;fGk!SeXQ$kGHosx?u#V_2R{gKRJB(a15sVJX6EAEDa#hJwD;H4=)Tz|?_;|j9 zmZ7AEHo@Zm$LP8VzwY#V5!CfHWgW?U?nDtOd%kq6BZ*Hn0; zz}~%kzr1PFrrm%1@kbVB9t4uaL2_o8gzGnkDg*~L;F&XLvcCK7yZ;Xk4h~WAIyI@S zJW9IW$*VUiM`IV5Nz^G;;@H*VZFc<|t11WrqA18fWOI@PF+%9U2HW3M-=Lh-#Gx@>4f93*Rn zjfx_2?b@~f`Th6bV{tS)H8m9}GA>u4Wks5ViM-7t@;1gAWgJAsDG0omHEY&v?9->u zKmw-~-ZO=(`qUOxbgz>)ZB&u63v}7_D^;r06f+O*k)VNt7A{;k53;sX5Z;VlDLp-% zkz105Fy|1U6u?Lv8eAEH-sbGtv*~#4^XAQ)U%h(u&fMeRJ>tC*I5nvaDwbxglV5M- zO0K#R6++?iAs_NcJhTOZwH-Kc;NTT2R;=E)Z(l@scz7meZ|Iq5F@gn9B2)#{P~ti0 zoU#%a^p5Ch5Snx6&Sl~?;g8%B(s|>GU=gyr+@R~r!y#d$Qj;oq_A_#@W&UxRR$f*p$? z4>KMF3&RTS`^an^=uz-@aRDX%E$;4x-sSrB>$vVJ1wR8n3qN!G`0*b!Z{EBYc^bT? zK)go0R=j4scDjcebWgg+DJGEIY1qCpWhPJ4Tu=iJli-=mK!H8eqeqXI$Bi2|X3?TW zi@*8in+=;cZ{E9m_wM5d4jj06`0(LBj~_oC3u$yBz;G8r&i(V}&)<)Th`0;jC7wKa zGBz|c^v1!12QTi~v*-Bt-+#Xs&#`#%;zf8)Jh!W>Ye)L5*7Vs%Pa{CnfUfmvy7nq` zFYY=x#S2nd%1F3Zsv^LIb3?um8XAKrZGoK!uUt1Np=v`Oq8)aA?b@}wf!7Fb-@bj{ z&Ye5=L&Nv@GyFdSq&Oo^Fl}JBvu%4pOQk?{?H^Cs9H~n9A`rW4lP%6_oE7E6_qtA3z zgeM&uCkIM|ii71&9bTR~--En{n(_ZHPk-YsfFi=815-+%Qo>b60h(xPzoR@)sXP7$ XmcK=JO$QQ000000NkvXXu0mjftF+^; literal 12857 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 wOwy|l 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 XmcK=JO$QQ000000NkvXXu0mjftF+^; literal 12857 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 wOwy|l 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 X9Ov?iYC7Du34ijRuxO^v1xlQLxjf}>y3q zK-WK!`E1+Qh{7ATJzdT3{Z^2SOgF3jZ0dsr+bR1HtCI&>-uc8Y_gEY~kd@`N;IrdX z(R?y<8^uNSDq4g6FHMwO2lxMf-4}@x>5o8@g)?#s|I4qLT9PM=5f-8{BbE=VXCZj0 zf?^KHvtz#tlkeD{Q}VY}v<^oc!wwtyRT8=uhD_49l1xii=b?v?o%G0;92OQ9-j-rf zntOhcDg?plZ3s%N)Ym{a{!tifhwbm*%%*3GVJ|4Ac@Da$UG0?&pi-aoQVOL564|SY zy#6-5tXYNxb!i=Cu{{eEkdTb#ot0uT!B;St2c)nCpm(2h-GpT6sde+l8V)(z@A1+~ zNN(1a1Pufn-Bc;qdnDAbcbn>z#3}Y5l!vfv2knHs2{CvGp(n_Gq^34ZE`c54(Yz7V zzei(Y0|WPBmak2k0aqvea?Va5<}+s@CZ76-G9>3Wn)jv+4yCu+yBEN16r08 zvz|r5-x-aZs!wI1v?9POVVj?yNag&r*w`mb%0Wo;RP*Lk)-}vD)963~f|3;Nmrh;C zFyx7OA!E@ur?4}zqxr}>mD6eFZD%c09Xd79BnF{8nO^~zGVjEWWaAWKrQtGcKzWj) z7;miIelCgL`;e4k5@j*!dd;4j8EIMuf94>0S@co#aAPMYekP9`ChNsjo$!tE@$rlZ zxaa4U&{%e8tEv;a`{)ah>ymDE|7Gv;_wV0()#;RZai!(9mDuxhHcG6Dql3f1Y_+Wh z?cFdXr0T6(r94(074Jk6fh?7Q4qd?W*=&(Ph7dWO91zT+LeTht2$jjxT&khh@zK=Z z|2#&~XLp<#UTyD59yzGyi!UCz=<1;xRes-A`t)bOshf+$b0^-xtgNgvGNr3nbRIW6 z(e`f`1y7sJ{{DWm=7;ec_=xA8!Xz95lIkAv;)qGGKRHPsJbaip_v4L=U~+zmWf-S)h*@x$0dlcfH;JOV z&SlEKiIJ2A_wd1kCvD4ekl<53$d#eK{=3AOnCDwe@cS!gRqv2yur999)hKGtE5EgY zETYI<0YvFgM~9{{?r%2?7qJg-^x0eJ4JW4dXZ?mua49w+X~FVy2fW>nGzT7YEj9=E zuptVgp;G6@K9bn>jGV!`Qzj|YFC42q>0kA;u6VXZ9#e*C3C$meZ(8=gG;aQS)K@AY zm|kVobdb&+W+%3MNT+nqq4VWmC-UFX#D*-?bFCq&w-gk9gjedJp{ximd7PcV+g)ZS zcYkSR<)ij*c!6V=(8iU^Dl~HX#AZ%F23BVcD?zHX>q)pCjUA zXVz?dwTN0-gksFOB1z>}?weqsXk@iNL)P<=!qICFSL-_YSGOZ`O(F=;>Fa$Bw|_Ew#hTvGb3GZsi;m1*5of2_)YyZ`X0aE zeVuBm6eX65W;nH$3uMwNMMoxe zWz?wcgYH3QSBer-#V2^83!-NKn9omLx%x*6-XF+pg&QBGuE_o~7nznVHi}#yU>EA$ zpR$q_h9x|1@mjy9@9XPp)-kic+#TEW0Am>qU3^(75|r`~?x!XW$wMz@h1$dez|}4+ zSQ}}r3BW@3teK!kgZ#)_*CZoK?y$@r`uutY$jBG;AvK<4s&{3^i$I(VPh>9Kw?=HG zb-_%ur#e1Cu-M(3GUp`?$s_a)+_nO3vI&cuia{TDPfgsQC zs_h2GELD{xm=Dr(aVmhNMqq~HftTM&kqg(4iLT~I01T%w#*A03x`hbsmTGbSQ)5wJ`>Yrf8qJCc~v)~@1LO2&LW|LHz zpyvH~-1=v!Y!Conz=HGQ1&XiZ)qxk?V%?pcy(T6mdk@sM_pO?*!CLD>6DyWR>4OpX zl9|vxt4b}B8!`dG5l5S)M8OfK4F*@wHbR84;qep$e1Nu#gAsDSp5*>44NTnmtp`Kn z>&>yIs+RMwRSvxVX2F-s7@+<8H`N;l&J25(Z0ko?rU%`j z6iEvtXi4=3xA_!EthwluMzm8@6DnkMU|`_?Vo~>!`e&o3#_;;XRkwl|vN&~`TSd=) z-_HU5EG*__15!+ofy{uTjpV@dqfKR`Rw6W;88M@(qSB%~oB8DSY$^|gh@zh8C*2WV zd$HtULDo6NfYyaeFDKikaRnd^$S!z{h3aCia z1E;1)DOal`^Nko^^1_??FQ%_Gv>yup&ixu%MY*TCK()u|ihx$Xf9C31jtaTHqNd3{ zW1@KYkG_Hh*820vd@m8kGHwHTeQ!bV?qXi5!i!M1`_6hx$1>8Y)WO!Lt?z3$;t5+` zjCDJ$>6D#xPs4hsA`1!%P9b*;9>m{Y3Ou}$b(nB05)2d2t-^ui&?b@2R&-?b8~ zMVkwVhdu}lLTJkvCIcD)^ymu1KFE3Tx%D?9 zW!a`8B0Rfaa2irJYP&|~v@CRS@)xr#4?xt8$y|L{d{3+H`R_06c4^{UR%eIZoFS+} zv$Iqqq;>jsJma)t*{qsM8jt!HSse|3h0bK84`qG3G zr^+pU4{l0E^S8cJuip1DJOi6nqQW_PCZWLn}#tXYNo(9 zt>Nbrf$66TDOadaOX%hN=yuQ8c(Jl!t~CWv*0wyjWJ)ra%-`$r^ZV9D zhM`_lb0-XBtg8BY%1HmT&#HCoP&jtJ9LVjU2-^F_TY8IgbpR`x#D4CJ=Fiw&{paodY%(LlwTeQX z>yI7tv&nIF@8;fPhN=87b8q6ohqfi)zuZOXw>4SO_MY;A(KL#Dvymm7X8fRHHX;nQs`GRL1MzfM zZSN-W&K7(dI;9jqE|S2|A~bzmt~tSBKgUk z={hB4YX5-HML_{<#W$rBT@HcpMM0Pv>syx2e!nATr!qR@OP;R??QtV?&&@~5CJU*=3i zoXiM9f+361FmwWKa&mGvo2*-0_V#KPuyGk%Vbj{e|Iz#uB5n*2;ao9Zn7w=CtFKDo zU$p8_f%PbpwSBhrkG-n|bFZkNs;bQ`5PRgd8gBbfZ4GBI&NisSLswU~21SJCD885D z&wB6Ne3%~5S1%~9rmEH~L&h6wG;)Y`FQmk~CJrC(_8M(5GvMMifjivZ`+qxEz)pMm z=?sV>SW*`@s}zB##xA1#yb3uoRA0~`dL9}t&L`xmHOg@abd>|{0vp}8?pA$1%qyOf z6@(0Aaebs+`2PI2LkAWt3VnqeBvR1#(Xp`s!-4@pSr<^swR6Zm+6)qMyHbGWi{|5^ zc%G7p@l$>GVZXpK)_EvV%P5IjR=&sp?B#4drLcy;=9Znd7nzrCX!=LS06paXfTiyxu$-y99NK5l&aL^3k`WXAlDun>B$PqzK>^Y;-s zyb75EwH@vx&_;d!9woNGrC$++tWr*|?~1I0gzWOfl1ihND+q&~`1@k_>BYh|@9p+%Qztn4OU~^X-ve8?F1k8z#I_|?ZQt7u%^Y^~& z?h}_tcifyF{c&5cjBSjUG)^0MBx+V^rI)Q7;O*XV5U|(meCq1K{Xs#Dk%?)N7M*7c zhh_$Zdq4KYRX(`qUiEkXEV5A_d#D^p zpKn#^B46{;BA&VT>QA}Iv`@f-y&L4VOgF+AcW8CCMEJ$&5p_!>Nf5ed0p!yyC|N2@ zsM)?dfeN^Kq(PgX0o`WD5SJX2Jgzr*g5?WjEP%zTDEG-W4D;RrJ8(C}Ny^FyBfK)iRW#Kw%EBDGZj}qeo~^~bUkQ+n z*8n=UK_TLS^FZb(eGNMu-Ge?`Xt_=6m6qHRK>-aHuSSZQD4~b<)Uza))t#c^pDiWQ z-8aj}STYvLk&D1ytX@i>e-+#&&Qtm4Yt0n*F7!s`GYae>EIKAe55JOX{6pRlNlMS3 z$u}}qWRTt7yL-ct0}$Q+1i$@(0I~CBjPggpFNG4Iwb|cnFiMET&BgxpX6X z6;v()(Mf9g09d?D>TM zM+vu0ol_1t!Gy8g?vS*d5rbk}Eldy;-cuEU*H@Qsv@K|zVqpf(P+i5c?^y#7RoP#Dsn*BV7@_T6A z>@Rbtr$R0ehduZ{9B~OlGA~sxv2t*zd?3lc=rZ^2<;Hd`4nvUMugi^oe3@#nw~vd# z&OQDOBmMY?WeZeeEI`V2&1NSjC|FY74%_%sE8r9=1aemDoY2R5+KB|^Yov6LBrYTE z??X3_o}hic8=b>^QQkzT2CmSDPk3|Xz1Bu`ID@2VB<8^6mrS6`_xS0(FiHhdn98O)g2R&$@McuE=P2OFAWM~u&t|{04WCvm+Dp+uB4=778H*< zkI}0H1OyhWZ|9ps$%tVvhT>eI33__^Q(B1O?rRANjoMvH;906+uOHF{kj7D_}acW z0&X&;1@+XKH`yHG1|7q}oAEssprMaW<}!xiwyIiJL@n#_VbgpR0V@$eCt?H5Y$hLY zvQ2EoFW5gX<~l>KclBHasBJ#cW-ZA6GkrThd1nwi?j4l{ySCZ);f`X514L*GP+`j8 z_Rfg`<1l-@2k=&Et47&(2y4=8cj-vV;$dt|P!F4L3#?9$q%LIps+;s8`zCc$F(F%5 zqS{cMBUOceV6HhD=BWfJ*)@PCSJ|@&s(ApdVaCP4!$Bt+lItb6(CV?=Lj8l2JxF8d zJD}nDJxrNf&30{De$2wM$P6?%6S2f%czAf+Kyhx{v7@pe6=7O>pOcew0QMLi z_L-QP1OG1}wE70=9Wek=U+3ilPs`!DInVK&OzHIEv_mOW`8-dbtD2~Xxn^r;=V@Ou zL&;+y2Ft!tZTpGaHZ4lObC!W3_H)NiMen)IFzai_bu-1nGs5@6S}W+Kx|;r7TQ<1$ zx=d9*O~~ZiX7Z6He33@_Z{$EYMwa>#}w=_Czdpd|DJviRZ}38c+0 zrsdi=?(D*G7vEa-JpbB2u}|m`%wksJLHnJ|?z~R3buMNSw^_N$3~2~yt?BsBtvG7h!~6(^AKjw{WGotoMZeCYzYc>jY*{z!L#zNB7L_4UR`g-X!*cb$PX z83G4Jl7dJf%X(KnUn{9Qcf4yA(=?|I!d}KgM&ciP$C(_SUkNFyD>PbCB*4m4Q&!-# zES7}mM$xg!3d{EU*St=8*UW>l_+F~FHML^mrIPrg$c=LXcBi=_@gX<)Bh3_zjBp>S z?~p(N_cR7*l?{tXFZJ%btzkxVZ!GieE_|y`f4b-^Zo?Q94Be+TU7!`qlu7zEGRNCC z^UR=!D{Y%50Y<}O>{dw?DBQd3+`3ZpXOMEJY5S#KFw5;D)yZpiMepslLcP>5nldm^ zj#rLCa!HbI68rl3rF|-Lth-CJGwG!u&4@bQf2K#3n6mPoL137r{meUv7~}d(0Gl%+ z9!uCoTC6dDW`KfEWfV!3%K6CNkfB#m4N-_%Gm$me33?t#AHeu zzx=cBdb_e?@Q|}Qy@XhwU~j?KG4NLUOmb|_v?!HM7@<2_mu9{GKwt}GuLqyCVq3K1 z=u8b%Vz11)Ag(Iti{(VsT@WFD?tf~Z(xySp=A81A*rNPYEfqh(o{C5J77;x!O~DJt z!Q5p;8;uUsG0N#JtLeOYw4)u~Cylo40YGGZ3KTS9iV5e?PW+wtF zDja31r^)!zznhyDR~84?Pb5norScnnwTplA?AeAQ%j*qMz84F(f~${-{xoX6P_1sN zH9Tl;{b(FEFAVp45es$oqVn9h?rJhZxaQ&&LatCsAZIhC&CY-H)x;q0^APsRbo0N` ztEKn{4pjpZ1+jk>&&6g1vv+9cqUv=z2lvA;`*T$yqT@NDQ77eL$@#%;-htLsq5I)- z%K;%)_-~-*?-Cjs`jRGF=EZo)QTe8^5i88JKKSx{K`{K@N4XlLHsq2bogKKGFb2|K@xn4_1_wgc@4+O{8cMa+8^ea5P zT1mQlb#=u;JpLBZ==0#ggU`7RNq&jqGko7T34$AnFG6jzUvJ-NcID8GhOIkC{W1+> ze_>JWNn#J>CF&|ak{1&CXX4?374LA!QF!xJ!SZUQhv+e~;8E#=p6|%a%uG=^GBk0Y zruM&3K;v8&=6@@|1s)s%Y*Q(tYT>u9`W~EbZ?i66Zo$t6{RtK6SVR>y4+IXuF?$e6 z$rh|5!oyFSJv4Ata_?Pe8tCQRA-YkQAz~qUGFqU+-y8=uZqQBn7RovP$A61=lHCWU zWUcZrCH13K@W*5-IIadkU@ubCg`O7t!+mt68@Ak?XmEW#)w-ZAa(;eps^h9)e;_=i zSL3Bq;2oGTqxtuHL;n zx8>+EU7{$GcpRl#r_VjWn%04YQ_{pfs*c$3n@Zv$kRg2 zWPsPY#x+c$aryTpFwFr>Gx(TWZtcWtx9|0Tv6M#D+J|A|(+}3d-fNKa|I3CAT!5BF zzm(Ge4v=7>p}`c81Rq8KrfuTlRKl7rJOs83(P(7HgboCb?x~!aWv_`BGiTGB#J@1) zii6S6!};Y@6~DVzz^^Ym!)}tUC5#D>W}bG(RDm{2E_7Q)og)3#!!F_0Fq=UHi=x}v z#RV?}-+Hn8vw=ei-F#&A)9?l<_K7nUR_MaNCbFR%!sOyD1Gzd`b|r~>_)(RPi}Y%; z5n&&umHN6#*nuepv(lSt@w?dgnxtlZeLZThs&CyHTgFI{!N0ad{LJ@c`%iNtkB+4K zu!E-sG;X(WemQS%Yq&w)fqc;>YSGNN;hAsof6Y(FEqTS~15-Ks2W6|2<;uPd4FH;Cx#f_9q#yW&UzC7Rb)_eb|W z((_s8^+pr`PQLa8>Q1bU5vRAGBw@_HXbuL%fK}d49Xv@*NqM<; zb$PKyC^cQy$*2<=E5LV#Q_)kYpPEVV@`k>ocD79kTmvR1GY|4g8Uq5PQR}7iXZEAm z95lv@s`RzW+N9h2IpF=XYft0UPDp^Y~<&EK^@Y+nq%c*%J3yv`bLjIE7eo^V+8@xZS)^S z9^?kUz<}6hc&A$|lju2zgr@n|d(%Q^N@KZH#2wOBOT_L(cL(miBYw+3mtJ-Rnc%9j zY2`4;$>xm&4C8>`oq99TIr#ax1W3Hc6m#fLHIjK5q5NCz*r7crxhp>r0;z#K> z+D&%T-g=wLG+j3T6Ku?DzFb^f`J&I7>E%X2r@G}s_;6S?@A&X=OYN5NmkVV^ zdg~uN$S0j8@*GGsQPKV7T@QAZK%8Xd9s}IN7z@gA^v?d*+jC7`oE@6S|0dWxe_S`2 z`mdQOA*|IcDxSz#4NW=>WJ-j8{`}d^_8+-i(kN^3gjoas+n6VB zX`lWJ4)_b1w3}}L@7G8iLrO}@%0Wv@E3AvTldDQEOyCXTDPk%QX*+axckw29dVED0 zjkf*<_v&T|gWVy)>T|n-JG^aRqqHHAXNY~Xs7}Rdd6KlAm?v)g%bW^d*OZ?ze9G+K z#zoA@P^%}2|MljpX~hfvyWot~om*yX)^KwAqF1O(nXw~bdmb=b|FUM_@ON{ShB+ej zX=fZ!uw@pvh{snA>-;~_w7f(+vkiNa)+KRq6edc}6wQgJ_kW2R%D$XA>PCkWm>kN6 z2Q0*)ZRSZ>^PKe)!_GztpHF_$h}s3ookHkl4Om1X687bLtk$pX8cS9|FGFd*^6Ae7 z-^Bkqua;ZzyVu20qit2`9HK_#>;rHlcg?M*`QGA>cAGtx!s6;Q{GXwOMVxKf(or83 z-jK;w%dPC}%yaC$OcQ$i;T5Jzm&Jn(TPFH>NiXRpJ%g}m`2?7LAxd6radD3e!(iuR z-Zk-h4i1GVhu#z$taZwoyZ$#F(kmX&MYe>Ccf)dXbK_v5Ok!4Y7aOAmrE_nyqRFks zw`vj0wSEDoyFrqmoaK(?_}g3<&?ntG8*UQ|6A|yKw24W_1PI8q4+z?hk zTMc;Bd~Z2&0wgfaGK^FxA7AQy>jz&`2~kk|9jJXe&#|aA$~R~vZg5Hc&fav5ebr#L zEawAV9*G-`(No<|u0U|nUuIU7umOTS8r}%n{Td(Kr1C(b+4pX6e-!q z4{cg6n^+Q}qoWy2jfAwpF>w6KhT-E}7_EZi?p#y9-d-)DL2SPL>uckAo{7!!-fstw zn8TF02-Gz0IKnd}9s4>)Mn=4*#i(a1F-JdGD9PrwoWB$n_Lz6Kf?^{}qd+Ql0i*mm zfKlyqC^mbE5#y8Z`WeQkWlODl=xvzK$7a8!HJ;9AknY-S*ZFQj{6?YOW9~{po|{6* z4k&F|o=tE=U#p1y^!5;@svdL#^WyTm9f z?9YG~B;^JIs#wg2hBZ_${k?-nMFE}8{xDpggi+t$Fwc+hUmrb#f~7a+fBKAkeOz`Y zDk@4~`ELN~`D+3~T6RF0IPjK~)WeqG%OC&Qnk$a|=`Xkb)UI=_9q-R9E9VEa!Hwnb z?Zp}zt8@@L3T9ZvOHx2MI5^^MN!;`b?w0kY3baBrj#@xmKp|4j%M(r87L3WJ!Id1= zNF9QJj~Yfy9EGXtbcvg~Q6=wp`Q-I&rOgGBW*Z@@AI8n}3U}*!`KmGW-?80hb3> zce9g}2;U2>>JTuo;liC?ME`gnjVw^-qO-j`KPJ9=L%HeoCY?c!)BZtEPEY=Oc^Zt9 ztvwKgto80tK7pu2QP||9EyKZtcfhb32Q-_5 zt_T)na}65Rf!Cl}A}QAHu=(&#n)@Rj@sV0ncpNTMUJ>Ly_=~+(fpYU2yKcB#&vNZH zRbP%a+$V7JhS_HqG1Qe8JXG>&pQM(xw6wiKrC2AispiIF>otauoJj<1V6&Z|c&o=E z{<{QiP$)trtqp0G&x={$u;<0?egVZ9tMj#NchjSdQTu8rPE>b&s>sD6yi&9uEBHGb ziXZlqzTfv*$SM%Q52ad0h_ceE4cA|96eOxx)xDUg1hb&q&cJQF^}Y7sP|#Kc%?oRe zR)S#KxDbz~TSFNbpNM1+X5IEg>79}wrCE9`b>MU~ z;FLfsWL7!%F3eGi^#{065z-aMdQE=rL(NM-G(^p*BHa&H%4B{__t!5;eSg6M@7zB8 zE~NWI;J^yk#@)Xil@_&5!$+IHTerdPK(oGMiK#=o9E25laUSAS*s=X%GRHKm9G5*j ze6-Wi7z6)$6~B1UMBqY%T6M z8F_r7>Y{GMCpx5NvB8zU@i7P#xpMTa*B*|@4-(VSv?bhCN_b$-z(PyAFDNa2L>Aly ziiaX(R8+ygr=}W&FE}WUj&5cRlI9U$lAE*d-pMI1X6d!v4#c) zQvJ8;z`YS21aI>Fs)E3~XAz29n4WNlc9RXz$0{YJVsE5Iw7%Vo&x5G;XG+*xcMnH8 za-&dfd&tEIwZ-Ve6T`Jf1uiDJmGMAJTMRnPFwS8utiov4C!V>-onVps zX#QcKqdT}GSj-B_c27~Rf4aw&PaMnyf$*~MlAx^g-BZXE_SK?6eJmY;iw;p ziS6y}%b2}}SF5w&6s-hGhyMPjlwDkatW)-{{L|UrR(Lyaf@rmi&`ir(6gM%-h zphq%{3%Ny779PULJt9rP&k{n9;Fp#;Y^?ZO;;sP4chWF2hqo5`Ts`_}LK6=n7zUuE zg%!%m%8GZMjpUnw1x5@YqfWr`@ddSSozF}5c(yC*yzt>7yIN-X`2~Ga??87v#rx*A ztQjX9>wfK+^U1nW(3^8NMj2EPEhO-?|HgiS`YT3B-(<~_y9!wXZ~fk?^}gM**0X5< zhVgbF&a}})q=V{US{N4M;t@oC!FqM@XrKgX3P&tr-t~yQW9nzCu!4; zegGHlqXUHRBP?Q&n_65I`bbAj+cl%E>v-iVq2|fhp~o>pPeT{VM4EkW4Yij+tdZ92 z_qfATXkHWw-bIM?#VfhVMc%gn&rsi81(JLJsFmD z|F$J-4dT|ZRia-ZuOZ&A8riwO_n4T6i@eAeb0t`TV(%$ksa7n)vP+V*YQW8OKu_zQ KW~DkJ?0*0q_Y?{M literal 19153 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index aac9984829bee5b93aa34a389661e78835495f07..f5f397f0684054be1cf13551907cec93e288d645 100644 GIT binary patch literal 8527 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 b8`+z?DXB+xXkE*h0uyYk>@7(a$iMy%=9&Ai literal 23877 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�cE5b)HbAWylI>H@;f0luiA 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 B>G=Qv diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 3ccac23ed870ad609479a6d522871a80da7c0353..fda6f29e1549ce87407f6ad6f1dcbf2919b09808 100644 GIT binary patch literal 11968 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 KW~DkJ?0*0q_Y?{M literal 19153 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 diff --git a/gradle.properties b/gradle.properties index a25e163ff..e390762d5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.parallel=true jvmArgs='-Xmx2048m' android.useAndroidX=true android.enabelR8=true -android.enableR8.fullMode=false +android.enableR8.fullMode=true android.enableJetifier=true android.debug.obsoleteApi=true android.enableBuildCache=true From 46dc5eb58e1d473279ccb3037a8286b7244e23f2 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 16:23:23 +0530 Subject: [PATCH 17/72] Fix crashing on About on release build --- app/build.gradle | 2 +- app/src/main/assets/contributors.json | 8 ++--- .../code/name/monkey/retromusic/MainModule.kt | 4 +++ .../retromusic/adapter/ContributorAdapter.kt | 7 +++- .../retromusic/fragments/LibraryViewModel.kt | 5 ++- .../fragments/about/AboutFragment.kt | 36 +++++-------------- .../monkey/retromusic/model/Contributor.kt | 16 +++++---- .../repository/LocalDataRepository.kt | 25 +++++++++++++ .../retromusic/repository/Repository.kt | 6 +++- 9 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/repository/LocalDataRepository.kt diff --git a/app/build.gradle b/app/build.gradle index dd90ecf6e..ef46eb094 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { } signingConfigs { release { - Properties properties = getProperties('/Users/apple/Documents/Github/retro.properties ') + Properties properties = getProperties('C:/Users/h4h13/Documents/GitHub/retro.properties ') storeFile file(getProperty(properties, 'storeFile')) keyAlias getProperty(properties, 'keyAlias') storePassword getProperty(properties, 'storePassword') diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json index b76201619..217d718a5 100644 --- a/app/src/main/assets/contributors.json +++ b/app/src/main/assets/contributors.json @@ -3,24 +3,24 @@ "name": "Hemanth Savarala", "summary": "Lead Developer & Designer", "link": "https://github.com/h4h13", - "profile_image": "https://i.imgur.com/AoVs9oj.jpg" + "image": "https://i.imgur.com/AoVs9oj.jpg" }, { "name": "Lennart Glamann", "summary": "Play Store banner and Images", "link": "https://t.me/FlixbusLennart", - "profile_image": "https://i.imgur.com/Q5Nsx1R.jpg" + "image": "https://i.imgur.com/Q5Nsx1R.jpg" }, { "name": "Daksh P. Jain", "summary": "Support Representative & Moderator", "link": "https://daksh.eu.org", - "profile_image": "https://i.imgur.com/fnYpg65.jpg" + "image": "https://i.imgur.com/fnYpg65.jpg" }, { "name": "Milind Goel", "summary": "Support Representative & Moderator", "link": "https://t.me/MilindGoel15", - "profile_image": "https://i.imgur.com/Bz4De21_d.jpg" + "image": "https://i.imgur.com/Bz4De21_d.jpg" } ] diff --git a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt index 4552702ae..cfe9c54ed 100644 --- a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt +++ b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt @@ -108,6 +108,7 @@ private val dataModule = module { get(), get(), get(), + get(), get() ) } bind Repository::class @@ -153,6 +154,9 @@ private val dataModule = module { get() ) } + single { + RealLocalDataRepository(get()) + } bind LocalDataRepository::class } private val viewModules = module { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt index ca7d604a4..84290ffe6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt @@ -59,6 +59,11 @@ class ContributorAdapter( return contributors.size } + fun swapData(it: List) { + contributors = it + notifyDataSetChanged() + } + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.findViewById(R.id.title) val text: TextView = itemView.findViewById(R.id.text) @@ -68,7 +73,7 @@ class ContributorAdapter( title.text = contributor.name text.text = contributor.summary Glide.with(image.context) - .load(contributor.profileImage) + .load(contributor.image) .error(R.drawable.ic_account) .placeholder(R.drawable.ic_account) .dontAnimate() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 248951353..2109bde0e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -32,7 +32,6 @@ class LibraryViewModel( private val legacyPlaylists = MutableLiveData>() private val genres = MutableLiveData>() private val searchResults = MutableLiveData>() - val paletteColor: LiveData = _paletteColor val panelState: MutableLiveData = MutableLiveData() @@ -293,6 +292,10 @@ class LibraryViewModel( fun artist(artistId: Long): LiveData = liveData { emit(repository.artistById(artistId)) } + + fun fetchContributors(): LiveData> = liveData { + emit(repository.contributor()) + } } enum class ReloadType { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt index 5cbe0b76b..abfea08e9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt @@ -13,18 +13,17 @@ import code.name.monkey.retromusic.App import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.ContributorAdapter -import code.name.monkey.retromusic.model.Contributor +import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.util.NavigationUtil -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import kotlinx.android.synthetic.main.card_credit.* import kotlinx.android.synthetic.main.card_other.* import kotlinx.android.synthetic.main.card_retro_info.* import kotlinx.android.synthetic.main.card_social.* -import java.io.IOException -import java.nio.charset.StandardCharsets +import org.koin.androidx.viewmodel.ext.android.sharedViewModel class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener { + private val libraryViewModel by sharedViewModel() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) version.setSummary(getAppVersion()) @@ -32,23 +31,6 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener { loadContributors() } - private val contributorsJson: String? - get() { - val json: String - try { - val inputStream = requireActivity().assets.open("contributors.json") - val size = inputStream.available() - val buffer = ByteArray(size) - inputStream.read(buffer) - inputStream.close() - json = String(buffer, StandardCharsets.UTF_8) - } catch (ex: IOException) { - ex.printStackTrace() - return null - } - return json - } - private fun openUrl(url: String) { val i = Intent(Intent.ACTION_VIEW) @@ -111,16 +93,14 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener { } private fun loadContributors() { - val type = object : TypeToken>() { - - }.type - val contributors = Gson().fromJson>(contributorsJson, type) - - val contributorAdapter = ContributorAdapter(contributors) + val contributorAdapter = ContributorAdapter(emptyList()) recyclerView.apply { layoutManager = LinearLayoutManager(requireContext()) itemAnimator = DefaultItemAnimator() adapter = contributorAdapter } + libraryViewModel.fetchContributors().observe(viewLifecycleOwner, { contributors -> + contributorAdapter.swapData(contributors) + }) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Contributor.kt b/app/src/main/java/code/name/monkey/retromusic/model/Contributor.kt index 6ea869f3f..23f952a35 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Contributor.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/Contributor.kt @@ -14,12 +14,14 @@ package code.name.monkey.retromusic.model +import android.os.Parcelable import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize -data class Contributor( - val name: String, - val summary: String, - val link: String, - @SerializedName("profile_image") - val profileImage: String -) \ No newline at end of file +@Parcelize +class Contributor( + @SerializedName("name") val name: String = "", + @SerializedName("summary") val summary: String = "", + @SerializedName("link") val link: String = "", + @SerializedName("image") val image: String = "" +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/LocalDataRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/LocalDataRepository.kt new file mode 100644 index 000000000..a40633051 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/repository/LocalDataRepository.kt @@ -0,0 +1,25 @@ +package code.name.monkey.retromusic.repository + +import android.content.Context +import code.name.monkey.retromusic.model.Contributor +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken + +interface LocalDataRepository { + fun contributors(): List +} + +class RealLocalDataRepository( + private val context: Context +) : LocalDataRepository { + + override fun contributors(): List { + val jsonString = context.assets.open("contributors.json") + .bufferedReader().use { it.readText() } + + val gsonBuilder = GsonBuilder() + val gson = gsonBuilder.create() + val listContributorType = object : TypeToken>() {}.type + return gson.fromJson(jsonString, listContributorType) + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt index fb549aa47..9264198a5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt @@ -98,6 +98,7 @@ interface Repository { suspend fun blackListPaths(): List suspend fun lyrics(artist: String, title: String): Result suspend fun deleteSongs(songs: List) + suspend fun contributor(): List } class RealRepository( @@ -112,7 +113,8 @@ class RealRepository( private val searchRepository: RealSearchRepository, private val topPlayedRepository: TopPlayedRepository, private val roomRepository: RoomRepository, - private val lyricsRestService: LyricsRestService + private val lyricsRestService: LyricsRestService, + private val localDataRepository: LocalDataRepository ) : Repository { override suspend fun lyrics(artist: String, title: String): Result = try { @@ -124,6 +126,8 @@ class RealRepository( override suspend fun deleteSongs(songs: List) = roomRepository.deleteSongs(songs) + override suspend fun contributor(): List = localDataRepository.contributors() + override suspend fun fetchAlbums(): List = albumRepository.albums() override suspend fun albumByIdAsync(albumId: Long): Album = albumRepository.album(albumId) From 870cf4ce2e5cf2c782db00a59f8401f56debe5b7 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 16:30:54 +0530 Subject: [PATCH 18/72] alpha release --- app/build.gradle | 6 +++--- .../monkey/retromusic/fragments/library/LibraryFragment.kt | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ef46eb094..4e03124d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10442 - versionName '3.6.000_hot_fix' + "_" + getDate() + versionCode 10443 + versionName '3.6.000' + "_" + getDate() multiDexEnabled true @@ -88,7 +88,7 @@ static def getProperty(Properties properties, String name) { } static def getDate() { - new Date().format('MMddyyyy') + new Date().format('MMddyyyyss') } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 818f52aa7..1b2144aa1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -55,6 +55,9 @@ class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { private fun setupNavigationController() { val navController = findNavController(R.id.fragment_container) NavigationUI.setupWithNavController(mainActivity.getBottomNavigationView(), navController) + navController.addOnDestinationChangedListener { controller, destination, arguments -> + appBarLayout.setExpanded(true,true) + } } override fun onPrepareOptionsMenu(menu: Menu) { From 0e8010ad785f99f7f2ff6726b2bbb1cf69bdb7ce Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 17:39:05 +0530 Subject: [PATCH 19/72] Fix issues --- app/proguard-rules.pro | 17 +++--- .../code/name/monkey/retromusic/MainModule.kt | 13 ++-- .../activities/PermissionActivity.kt | 2 +- .../monkey/retromusic/dialogs/LyricsDialog.kt | 60 ------------------- .../fragments/albums/AlbumsFragment.kt | 5 -- .../fragments/artists/ArtistsFragment.kt | 5 -- .../fragments/genres/GenresFragment.kt | 6 -- .../retromusic/fragments/home/HomeFragment.kt | 5 -- .../fragments/playlists/PlaylistsFragment.kt | 5 -- .../fragments/songs/SongsFragment.kt | 6 -- .../monkey/retromusic/model/DeezerResponse.kt | 10 ++++ .../retromusic/repository/Repository.kt | 10 +--- 12 files changed, 24 insertions(+), 120 deletions(-) delete mode 100644 app/src/main/java/code/name/monkey/retromusic/dialogs/LyricsDialog.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 950e401f8..0608eae5c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,13 +26,11 @@ -dontwarn java.lang.invoke.* -dontwarn **$$Lambda$* +-dontwarn javax.annotation.** # RetroFit -dontwarn retrofit.** -keep class retrofit.** { *; } --keepattributes Signature --keepattributes Exceptions --dontwarn javax.annotation.** # Glide -keep public class * implements com.bumptech.glide.module.GlideModule @@ -41,17 +39,18 @@ public *; } +# OkHttp +-keepattributes Signature +-keepattributes *Annotation* +-keep interface com.squareup.okhttp3.** { *; } +-dontwarn com.squareup.okhttp3.** -dontwarn -ignorewarnings -#-dontwarn android.support.v8.renderscript.* -#-keepclassmembers class android.support.v8.renderscript.RenderScript { -# native *** rsn*(...); -# native *** n*(...); -#} +-dontwarn org.jaudiotagger.** +-keep class org.jaudiotagger.** { *; } -#-keep class org.jaudiotagger.** { *; } -keepclassmembers enum * { *; } -keepattributes *Annotation*, Signature, Exception -keepnames class androidx.navigation.fragment.NavHostFragment diff --git a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt index cfe9c54ed..c701a47ec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt +++ b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt @@ -13,7 +13,10 @@ import code.name.monkey.retromusic.fragments.artists.ArtistDetailsViewModel import code.name.monkey.retromusic.fragments.genres.GenreDetailsViewModel import code.name.monkey.retromusic.fragments.playlists.PlaylistDetailsViewModel import code.name.monkey.retromusic.model.Genre -import code.name.monkey.retromusic.network.* +import code.name.monkey.retromusic.network.provideDefaultCache +import code.name.monkey.retromusic.network.provideLastFmRest +import code.name.monkey.retromusic.network.provideLastFmRetrofit +import code.name.monkey.retromusic.network.provideOkHttp import code.name.monkey.retromusic.repository.* import code.name.monkey.retromusic.util.FilePathUtil import kotlinx.coroutines.Dispatchers.IO @@ -35,15 +38,9 @@ val networkModule = module { single { provideLastFmRetrofit(get()) } - single { - provideDeezerRest(get()) - } single { provideLastFmRest(get()) } - single { - provideLyrics(get()) - } } private val roomModule = module { @@ -92,7 +89,6 @@ private val mainModule = module { single { androidContext().contentResolver } - } private val dataModule = module { single { @@ -109,7 +105,6 @@ private val dataModule = module { get(), get(), get(), - get() ) } bind Repository::class diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt index b448836a1..de64f8f0a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt @@ -39,7 +39,7 @@ class PermissionActivity : AbsMusicServiceActivity() { } finish.accentBackgroundColor() finish.setOnClickListener { - if (hasPermissions() && !RingtoneManager.requiresDialog(this)) { + if (hasPermissions() ) { startActivity( Intent(this, MainActivity::class.java).addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/LyricsDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/LyricsDialog.kt deleted file mode 100644 index 9c3b63c78..000000000 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/LyricsDialog.kt +++ /dev/null @@ -1,60 +0,0 @@ -package code.name.monkey.retromusic.dialogs - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope -import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.extensions.accentTextColor -import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.network.Result -import code.name.monkey.retromusic.repository.Repository -import kotlinx.android.synthetic.main.lyrics_dialog.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject - -class LyricsDialog : DialogFragment() { - override fun getTheme(): Int { - return R.style.MaterialAlertDialogTheme - } - - private val repository by inject() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.lyrics_dialog, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val song = MusicPlayerRemote.currentSong - dialogTitle.text = song.title - syncedLyrics.accentTextColor() - lifecycleScope.launch(IO) { - val result: Result = repository.lyrics( - song.artistName, - song.title - ) - withContext(Main) { - - when (result) { - is Result.Error -> progressBar.hide() - is Result.Loading -> println("Loading") - is Result.Success -> { - progressBar.hide() - lyricsText.text = result.data - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt index 251fc752c..d3ff1601d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt @@ -22,11 +22,6 @@ import com.google.android.material.transition.platform.MaterialFadeThrough class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), AlbumClickListener { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getAlbums().observe(viewLifecycleOwner, Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt index 7f162d93b..b518f7907 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt @@ -20,11 +20,6 @@ import com.google.android.material.transition.platform.MaterialFadeThrough class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment(), ArtistClickListener { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getArtists().observe(viewLifecycleOwner, Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt index 46d55d696..3574c8a9a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt @@ -21,14 +21,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.GenreAdapter import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewFragment -import com.google.android.material.transition.platform.MaterialFadeThrough class GenresFragment : AbsRecyclerViewFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getGenre().observe(viewLifecycleOwner, Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index c8d6dde53..60f5b7074 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -44,11 +44,6 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel class HomeFragment : AbsMainActivityFragment(if (PreferenceUtil.isHomeBanner) R.layout.fragment_banner_home else R.layout.fragment_home) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - private val libraryViewModel: LibraryViewModel by sharedViewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt index 932066a01..326e530c5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt @@ -15,11 +15,6 @@ import com.google.android.material.transition.platform.MaterialFadeThrough import kotlinx.android.synthetic.main.fragment_library.* class PlaylistsFragment : AbsRecyclerViewFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getPlaylists().observe(viewLifecycleOwner, Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/songs/SongsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/songs/SongsFragment.kt index 80ccb8d77..a7a02de9b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/songs/SongsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/songs/SongsFragment.kt @@ -12,15 +12,9 @@ import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewCustomGridSizeF import code.name.monkey.retromusic.helper.SortOrder.SongSortOrder import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil -import com.google.android.material.transition.platform.MaterialFadeThrough class SongsFragment : AbsRecyclerViewCustomGridSizeFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getSongs().observe(viewLifecycleOwner, Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/model/DeezerResponse.kt b/app/src/main/java/code/name/monkey/retromusic/model/DeezerResponse.kt index 929a9d131..7a24e9eec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/DeezerResponse.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/DeezerResponse.kt @@ -3,13 +3,17 @@ package code.name.monkey.retromusic.model import com.google.gson.annotations.SerializedName data class Data( + @SerializedName("id") val id: String, + @SerializedName("link") val link: String, + @SerializedName("name") val name: String, @SerializedName("nb_album") val nbAlbum: Int, @SerializedName("nb_fan") val nbFan: Int, + @SerializedName("picture") val picture: String, @SerializedName("picture_big") val pictureBig: String, @@ -19,13 +23,19 @@ data class Data( val pictureSmall: String, @SerializedName("picture_xl") val pictureXl: String, + @SerializedName("radio") val radio: Boolean, + @SerializedName("tracklist") val tracklist: String, + @SerializedName("type") val type: String ) data class DeezerResponse( + @SerializedName("data") val data: List, + @SerializedName("next") val next: String, + @SerializedName("total") val total: Int ) \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt index 9264198a5..2bf9c67ff 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt @@ -21,7 +21,6 @@ import code.name.monkey.retromusic.db.* import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.model.smartplaylist.NotPlayedPlaylist import code.name.monkey.retromusic.network.LastFMService -import code.name.monkey.retromusic.network.LyricsRestService import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.Result.* import code.name.monkey.retromusic.network.model.LastFmAlbum @@ -96,7 +95,6 @@ interface Repository { suspend fun checkSongExistInPlayCount(songId: Long): List suspend fun playCountSongs(): List suspend fun blackListPaths(): List - suspend fun lyrics(artist: String, title: String): Result suspend fun deleteSongs(songs: List) suspend fun contributor(): List } @@ -113,16 +111,10 @@ class RealRepository( private val searchRepository: RealSearchRepository, private val topPlayedRepository: TopPlayedRepository, private val roomRepository: RoomRepository, - private val lyricsRestService: LyricsRestService, private val localDataRepository: LocalDataRepository ) : Repository { - override suspend fun lyrics(artist: String, title: String): Result = try { - Success(lyricsRestService.getLyrics(artist, title)) - } catch (e: Exception) { - println(e) - Error(e) - } + override suspend fun deleteSongs(songs: List) = roomRepository.deleteSongs(songs) From b9c12e20ddc0cdd4f8870b90f384890e3d979014 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 20:51:08 +0530 Subject: [PATCH 20/72] Fix issues Lockscreen controls and screen wakes when locked Removed animaked `peakheight` for Bottomsheet Added padding for details items to show last item Fix suggestion text for less than 26 API --- app/build.gradle | 3 ++- app/proguard-rules.pro | 6 ++++-- app/src/main/AndroidManifest.xml | 7 ++----- .../activities/LockScreenActivity.kt | 14 +++++++------- .../base/AbsSlidingMusicPanelActivity.kt | 6 +++--- .../retromusic/fragments/DetailListFragment.kt | 16 +++++++++++++--- ...agment.kt => LockScreenControlsFragment.kt} | 2 +- .../retromusic/repository/SongRepository.kt | 18 +++++++++++------- .../retromusic/service/MusicService.java | 14 +++++++++++++- .../main/res/layout/activity_lock_screen.xml | 2 +- app/src/main/res/layout/item_suggestions.xml | 3 ++- 11 files changed, 59 insertions(+), 32 deletions(-) rename app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/{LockScreenPlayerControlsFragment.kt => LockScreenControlsFragment.kt} (99%) diff --git a/app/build.gradle b/app/build.gradle index 4e03124d4..44d5b4178 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,9 @@ android { } buildTypes { release { + debuggable true minifyEnabled true - shrinkResources true + //shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0608eae5c..c48afcd1b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -45,8 +45,10 @@ -keep interface com.squareup.okhttp3.** { *; } -dontwarn com.squareup.okhttp3.** --dontwarn --ignorewarnings +#-dontwarn +#-ignorewarnings +-dontshrink +-dontobfuscate -dontwarn org.jaudiotagger.** -keep class org.jaudiotagger.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 742fd5715..c8b117ede 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ package="code.name.monkey.retromusic"> + @@ -119,11 +120,7 @@ - - + = Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) - setTurnScreenOn(true) + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + keyguardManager.requestDismissKeyguard(this, null) } else { this.window.addFlags( WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED ) } setContentView(R.layout.activity_lock_screen) @@ -67,8 +68,7 @@ class LockScreenActivity : AbsMusicServiceActivity() { Slidr.attach(this, config) - fragment = - supportFragmentManager.findFragmentById(R.id.playback_controls_fragment) as LockScreenPlayerControlsFragment? + fragment = whichFragment(R.id.playback_controls_fragment) findViewById(R.id.slide).apply { translationY = 100f diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index fd58e7b7c..1f80bbc6d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -330,7 +330,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) bottomSheetBehavior.isHideable = true - bottomSheetBehavior.setPeekHeight(0, true) + bottomSheetBehavior.setPeekHeight(0, false) bottomSheetBehavior.state = STATE_COLLAPSED } COLLAPSED_WITH -> { @@ -341,7 +341,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomSheetBehavior.isHideable = false bottomSheetBehavior.setPeekHeight( if (isQueueEmpty) 0 else (heightOfBar * 2) - 24, - true + false ) bottomNavigationView.isVisible = true } @@ -353,7 +353,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomSheetBehavior.isHideable = false bottomSheetBehavior.setPeekHeight( if (isQueueEmpty) 0 else heightOfBar - 24, - true + false ) bottomNavigationView.isGone = true } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index 6c4c0510b..cd24eac7f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -10,11 +10,13 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import code.name.monkey.retromusic.* import code.name.monkey.retromusic.adapter.album.AlbumAdapter import code.name.monkey.retromusic.adapter.artist.ArtistAdapter import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.db.toSong +import code.name.monkey.retromusic.extensions.dipToPix import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment @@ -52,6 +54,15 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de LAST_ADDED_PLAYLIST -> lastAddedSongs() TOP_PLAYED_PLAYLIST -> topPlayed() } + + + recyclerView.adapter?.registerAdapterDataObserver(object : AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + val height = dipToPix(52f) + recyclerView.setPadding(0, 0, 0, height.toInt()) + } + }) } private fun lastAddedSongs() { @@ -84,7 +95,6 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de libraryViewModel.playCountSongs().observe(viewLifecycleOwner, Observer { songs -> songAdapter.swapDataSet(songs) }) - } private fun loadHistory() { @@ -116,8 +126,8 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - libraryViewModel.favorites().observe(viewLifecycleOwner, { - val songs = it.map { songEntity -> songEntity.toSong() } + libraryViewModel.favorites().observe(viewLifecycleOwner, { songEntities -> + val songs = songEntities.map { songEntity -> songEntity.toSong() } songAdapter.swapDataSet(songs) }) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenPlayerControlsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenControlsFragment.kt similarity index 99% rename from app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenPlayerControlsFragment.kt rename to app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenControlsFragment.kt index 51556b8a3..b4d395ea4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenPlayerControlsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/lockscreen/LockScreenControlsFragment.kt @@ -43,7 +43,7 @@ import kotlinx.android.synthetic.main.fragment_lock_screen_playback_controls.* /** * @author Hemanth S (h4h13). */ -class LockScreenPlayerControlsFragment : +class LockScreenControlsFragment : AbsPlayerControlsFragment(R.layout.fragment_lock_screen_playback_controls) { private var progressViewUpdateHelper: MusicProgressViewUpdateHelper? = null diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt index 872ac1d25..da52da288 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SongRepository.kt @@ -154,13 +154,17 @@ class RealSongRepository(private val context: Context) : SongRepository { } else { Media.EXTERNAL_CONTENT_URI } - return context.contentResolver.query( - uri, - baseProjection, - selectionFinal, - selectionValuesFinal, - sortOrder - ) + return try { + context.contentResolver.query( + uri, + baseProjection, + selectionFinal, + selectionValuesFinal, + sortOrder + ) + } catch (ex: SecurityException) { + return null + } } private fun generateBlacklistSelection( diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index 917e4c306..ebc0a9add 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -63,6 +63,7 @@ import java.util.Objects; import java.util.Random; import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.activities.LockScreenActivity; import code.name.monkey.retromusic.appwidgets.AppWidgetBig; import code.name.monkey.retromusic.appwidgets.AppWidgetCard; import code.name.monkey.retromusic.appwidgets.AppWidgetClassic; @@ -245,7 +246,16 @@ public class MusicService extends Service implements updateNotification(); } }; - + private final BroadcastReceiver lockScreenReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) { + Intent lockIntent = new Intent(context, LockScreenActivity.class); + lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(lockIntent); + } + } + }; private QueueSaveHandler queueSaveHandler; private HandlerThread queueSaveHandlerThread; private boolean queuesRestored; @@ -362,6 +372,7 @@ public class MusicService extends Service implements registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE)); registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); + registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); initNotification(); @@ -403,6 +414,7 @@ public class MusicService extends Service implements public void onDestroy() { unregisterReceiver(widgetIntentReceiver); unregisterReceiver(updateFavoriteReceiver); + unregisterReceiver(lockScreenReceiver); if (becomingNoisyReceiverRegistered) { unregisterReceiver(becomingNoisyReceiver); becomingNoisyReceiverRegistered = false; diff --git a/app/src/main/res/layout/activity_lock_screen.xml b/app/src/main/res/layout/activity_lock_screen.xml index f4f0872ae..0e707bd53 100644 --- a/app/src/main/res/layout/activity_lock_screen.xml +++ b/app/src/main/res/layout/activity_lock_screen.xml @@ -24,7 +24,7 @@ diff --git a/app/src/main/res/layout/item_suggestions.xml b/app/src/main/res/layout/item_suggestions.xml index 70f5a24c3..046d4dfb0 100644 --- a/app/src/main/res/layout/item_suggestions.xml +++ b/app/src/main/res/layout/item_suggestions.xml @@ -145,8 +145,9 @@ android:text="New music mix" android:textAppearance="@style/TextViewNormal" android:textStyle="bold" + android:textSize="32sp" app:autoSizeMaxTextSize="32sp" - app:autoSizeMinTextSize="18sp" + app:autoSizeMinTextSize="24sp" app:autoSizeStepGranularity="1sp" /> From 697da8a8f4b9e8d47b4e4c0db240e2780d11f895 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Thu, 24 Sep 2020 20:51:46 +0530 Subject: [PATCH 21/72] Update build.gradle --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4e03124d4..d2d6031f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { } signingConfigs { release { - Properties properties = getProperties('C:/Users/h4h13/Documents/GitHub/retro.properties ') + Properties properties = getProperties('/Users/apple/Documents/Github/retro.properties ') storeFile file(getProperty(properties, 'storeFile')) keyAlias getProperty(properties, 'keyAlias') storePassword getProperty(properties, 'storePassword') From 9850341d4ce189c394edfccdbc290dfc8c57bb29 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 25 Sep 2020 01:51:32 +0530 Subject: [PATCH 22/72] Updating Constants names Proper namings Bottom tabs slide animation Added home album list style change --- .../code/name/monkey/retromusic/Constants.kt | 4 +++ .../base/AbsSlidingMusicPanelActivity.kt | 29 ++++++------------- .../monkey/retromusic/adapter/HomeAdapter.kt | 4 +-- .../adapter/album/AlbumCoverPagerAdapter.kt | 4 +-- .../retromusic/extensions/ViewExtensions.kt | 13 +++++++++ .../fragments/settings/AbsSettingsFragment.kt | 1 - .../fragments/settings/AudioSettings.kt | 11 +++---- .../settings/ImageSettingFragment.kt | 5 ++-- .../settings/NotificationSettingsFragment.kt | 7 +++-- .../settings/NowPlayingSettingsFragment.kt | 6 ++-- .../settings/OtherSettingsFragment.kt | 9 ++++-- .../settings/PersonalizeSettingsFragment.kt | 15 ++++++---- .../settings/ThemeSettingsFragment.kt | 23 +++++++-------- .../monkey/retromusic/util/PreferenceUtil.kt | 21 ++++++++++---- .../layout-land/fragment_album_details.xml | 2 ++ .../layout-land/fragment_artist_details.xml | 2 ++ .../res/layout-land/fragment_banner_home.xml | 2 ++ .../main/res/layout-land/fragment_home.xml | 2 ++ .../res/layout/fragment_album_details.xml | 2 ++ .../res/layout/fragment_artist_details.xml | 2 ++ .../main/res/layout/fragment_banner_home.xml | 2 ++ app/src/main/res/layout/fragment_home.xml | 2 ++ .../main/res/master/values-af-rZA/strings.xml | 6 ++-- .../main/res/master/values-bn-rIN/strings.xml | 6 ++-- .../main/res/master/values-ca-rES/strings.xml | 6 ++-- .../main/res/master/values-cs-rCZ/strings.xml | 6 ++-- .../main/res/master/values-da-rDK/strings.xml | 6 ++-- .../main/res/master/values-el-rGR/strings.xml | 6 ++-- .../main/res/master/values-en-rUS/strings.xml | 6 ++-- .../main/res/master/values-fa-rIR/strings.xml | 6 ++-- .../main/res/master/values-fi-rFI/strings.xml | 6 ++-- .../main/res/master/values-hi-rIN/strings.xml | 6 ++-- .../main/res/master/values-it-rIT/strings.xml | 2 +- .../main/res/master/values-iw-rIL/strings.xml | 6 ++-- .../main/res/master/values-kn-rIN/strings.xml | 6 ++-- .../main/res/master/values-ko-rKR/strings.xml | 6 ++-- .../main/res/master/values-ml-rIN/strings.xml | 6 ++-- .../main/res/master/values-ne-rIN/strings.xml | 6 ++-- .../main/res/master/values-nl-rNL/strings.xml | 6 ++-- .../main/res/master/values-no-rNO/strings.xml | 6 ++-- .../main/res/master/values-or-rIN/strings.xml | 6 ++-- .../main/res/master/values-pt-rPT/strings.xml | 6 ++-- .../main/res/master/values-ro-rRO/strings.xml | 6 ++-- .../main/res/master/values-sk-rSK/strings.xml | 6 ++-- .../main/res/master/values-sr-rSP/strings.xml | 6 ++-- .../main/res/master/values-sv-rSE/strings.xml | 6 ++-- .../main/res/master/values-ta-rIN/strings.xml | 6 ++-- .../main/res/master/values-ur-rIN/strings.xml | 6 ++-- .../main/res/master/values-zh-rTW/strings.xml | 6 ++-- app/src/main/res/values-af-rZA/strings.xml | 6 ++-- app/src/main/res/values-bn-rIN/strings.xml | 6 ++-- app/src/main/res/values-ca-rES/strings.xml | 6 ++-- app/src/main/res/values-cs-rCZ/strings.xml | 6 ++-- app/src/main/res/values-da-rDK/strings.xml | 6 ++-- app/src/main/res/values-el-rGR/strings.xml | 6 ++-- app/src/main/res/values-en-rHK/strings.xml | 6 ++-- app/src/main/res/values-en-rID/strings.xml | 6 ++-- app/src/main/res/values-en-rIN/strings.xml | 6 ++-- app/src/main/res/values-en-rUS/strings.xml | 6 ++-- app/src/main/res/values-fa-rIR/strings.xml | 6 ++-- app/src/main/res/values-fi-rFI/strings.xml | 6 ++-- app/src/main/res/values-hi-rIN/strings.xml | 6 ++-- app/src/main/res/values-it-rIT/strings.xml | 2 +- app/src/main/res/values-iw-rIL/strings.xml | 6 ++-- app/src/main/res/values-kn-rIN/strings.xml | 6 ++-- app/src/main/res/values-ko-rKR/strings.xml | 6 ++-- app/src/main/res/values-ml-rIN/strings.xml | 6 ++-- app/src/main/res/values-ne-rIN/strings.xml | 6 ++-- app/src/main/res/values-nl-rNL/strings.xml | 6 ++-- app/src/main/res/values-no-rNO/strings.xml | 6 ++-- app/src/main/res/values-or-rIN/strings.xml | 6 ++-- app/src/main/res/values-pt-rPT/strings.xml | 6 ++-- app/src/main/res/values-ro-rRO/strings.xml | 6 ++-- app/src/main/res/values-sk-rSK/strings.xml | 6 ++-- app/src/main/res/values-sr-rSP/strings.xml | 6 ++-- app/src/main/res/values-ta-rIN/strings.xml | 6 ++-- app/src/main/res/values-ur-rIN/strings.xml | 6 ++-- app/src/main/res/values-zh-rTW/strings.xml | 6 ++-- app/src/main/res/values/strings.xml | 7 +++-- app/src/main/res/values/styles.xml | 8 +++++ app/src/main/res/xml/pref_ui.xml | 11 +++++++ 81 files changed, 291 insertions(+), 231 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index f0ce5dbd1..55023db2a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -76,6 +76,8 @@ const val BLURRED_ALBUM_ART = "blurred_album_art" const val NEW_BLUR_AMOUNT = "new_blur_amount" const val TOGGLE_HEADSET = "toggle_headset" const val GENERAL_THEME = "general_theme" +const val ACCENT_COLOR = "accent_color" +const val SHOULD_COLOR_APP_SHORTCUTS = "should_color_app_shortcuts" const val CIRCULAR_ALBUM_ART = "circular_album_art" const val USER_NAME = "user_name" const val TOGGLE_FULL_SCREEN = "toggle_full_screen" @@ -87,6 +89,7 @@ const val BANNER_IMAGE_PATH = "banner_image_path" const val ADAPTIVE_COLOR_APP = "adaptive_color_app" const val TOGGLE_SEPARATE_LINE = "toggle_separate_line" const val HOME_ARTIST_GRID_STYLE = "home_artist_grid_style" +const val HOME_ALBUM_GRID_STYLE = "home_album_grid_style" const val TOGGLE_ADD_CONTROLS = "toggle_add_controls" const val ALBUM_COVER_STYLE = "album_cover_style_id" const val ALBUM_COVER_TRANSFORM = "album_cover_transform" @@ -127,6 +130,7 @@ const val ALBUM_ARTISTS_ONLY = "album_artists_only" const val ALBUM_DETAIL_SONG_SORT_ORDER = "album_detail_song_sort_order" const val LYRICS_OPTIONS = "lyrics_tab_position" const val CHOOSE_EQUALIZER = "choose_equalizer" +const val EQUALIZER = "equalizer" const val TOGGLE_SHUFFLE = "toggle_shuffle" const val SONG_GRID_STYLE = "song_grid_style" const val PAUSE_ON_ZERO_VOLUME = "pause_on_zero_volume" diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 1f80bbc6d..f72a11594 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -8,16 +8,13 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.annotation.LayoutRes import androidx.core.view.ViewCompat -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior -import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.extensions.show -import code.name.monkey.retromusic.extensions.whichFragment +import code.name.monkey.retromusic.extensions.* import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment import code.name.monkey.retromusic.fragments.NowPlayingScreen @@ -322,43 +319,35 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { val isQueueEmpty = MusicPlayerRemote.playingQueue.isEmpty() when (state) { EXPAND -> { - println("EXPAND") expandPanel() } HIDE -> { - println("HIDE") ViewCompat.setElevation(slidingPanel, 0f) ViewCompat.setElevation(bottomNavigationView, 10f) bottomSheetBehavior.isHideable = true - bottomSheetBehavior.setPeekHeight(0, false) + bottomSheetBehavior.peekHeightAnimate(0) + bottomNavigationView.translateXAnimate(0f) bottomSheetBehavior.state = STATE_COLLAPSED } COLLAPSED_WITH -> { - println("COLLAPSED_WITH") val heightOfBar = bottomNavigationView.height + val height = if (isQueueEmpty) 0 else (heightOfBar * 2) - 24 ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) bottomSheetBehavior.isHideable = false - bottomSheetBehavior.setPeekHeight( - if (isQueueEmpty) 0 else (heightOfBar * 2) - 24, - false - ) - bottomNavigationView.isVisible = true + bottomSheetBehavior.peekHeightAnimate(height) + bottomNavigationView.translateXAnimate(0f) } COLLAPSED_WITHOUT -> { - println("COLLAPSED_WITHOUT") val heightOfBar = bottomNavigationView.height + val height = if (isQueueEmpty) 0 else heightOfBar - 24 ViewCompat.setElevation(bottomNavigationView, 10f) ViewCompat.setElevation(slidingPanel, 10f) bottomSheetBehavior.isHideable = false - bottomSheetBehavior.setPeekHeight( - if (isQueueEmpty) 0 else heightOfBar - 24, - false - ) - bottomNavigationView.isGone = true + bottomSheetBehavior.peekHeightAnimate(height) + bottomNavigationView.translateXAnimate(150f) } else -> { - println("ELSE") bottomSheetBehavior.isHideable = true bottomSheetBehavior.peekHeight = 0 collapsePanel() diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt index 17a7e407f..4c161daf7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt @@ -225,10 +225,10 @@ class HomeAdapter( } fun artistsAdapter(artists: List) = - ArtistAdapter(activity, artists, PreferenceUtil.homeGridStyle, null, this) + ArtistAdapter(activity, artists, PreferenceUtil.homeArtistGridStyle, null, this) fun albumAdapter(albums: List) = - AlbumAdapter(activity, albums, R.layout.item_image, null, this) + AlbumAdapter(activity, albums, PreferenceUtil.homeAlbumGridStyle, null, this) fun gridLayoutManager() = GridLayoutManager(activity, 1, GridLayoutManager.HORIZONTAL, false) fun linearLayoutManager() = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index 2ee9ddb22..11aa9560d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -98,14 +98,14 @@ class AlbumCoverPagerAdapter( private fun showLyricsDialog() { lifecycleScope.launch(Dispatchers.IO) { - val data: String = MusicUtil.getLyrics(song) ?: "No lyrics found" + val data: String? = MusicUtil.getLyrics(song) withContext(Dispatchers.Main) { MaterialAlertDialogBuilder( requireContext(), R.style.ThemeOverlay_MaterialComponents_Dialog_Alert ).apply { setTitle(song.title) - setMessage(data) + setMessage(if (data.isNullOrEmpty()) "No lyrics found" else data) setNegativeButton(R.string.synced_lyrics) { _, _ -> NavigationUtil.goToLyrics(requireActivity()) } diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 3c224ed6a..42c0d1d4c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -20,6 +20,8 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.annotation.LayoutRes +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.TintHelper import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -50,9 +52,20 @@ fun EditText.appHandleColor(): EditText { fun View.translateXAnimate(value: Float) { + println("translateXAnimate $value") ObjectAnimator.ofFloat(this, "translationY", value) .apply { duration = 300 + doOnStart { + if (value == 0f) { + show() + } + } + doOnEnd { + if (value != 0f) { + hide() + } + } start() } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AbsSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AbsSettingsFragment.kt index 7f8105ddb..d7669f90a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AbsSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AbsSettingsFragment.kt @@ -63,7 +63,6 @@ abstract class AbsSettingsFragment : ATEPreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setDivider(ColorDrawable(Color.TRANSPARENT)) - //listView.setBackgroundColor(ATHUtil.resolveColor(requireContext(), R.attr.colorSurface)) listView.overScrollMode = View.OVER_SCROLL_NEVER listView.setPadding(0, 0, 0, 0) listView.setPaddingRelative(0, 0, 0, 0) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AudioSettings.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AudioSettings.kt index be06d30da..123da7aea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AudioSettings.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/AudioSettings.kt @@ -18,6 +18,7 @@ import android.content.Intent import android.media.audiofx.AudioEffect import android.os.Bundle import androidx.preference.Preference +import code.name.monkey.retromusic.EQUALIZER import code.name.monkey.retromusic.R import code.name.monkey.retromusic.util.NavigationUtil @@ -28,14 +29,14 @@ import code.name.monkey.retromusic.util.NavigationUtil class AudioSettings : AbsSettingsFragment() { override fun invalidateSettings() { - val findPreference: Preference = findPreference("equalizer")!! + val findPreference: Preference? = findPreference(EQUALIZER) if (!hasEqualizer()) { - findPreference.isEnabled = false - findPreference.summary = resources.getString(R.string.no_equalizer) + findPreference?.isEnabled = false + findPreference?.summary = resources.getString(R.string.no_equalizer) } else { - findPreference.isEnabled = true + findPreference?.isEnabled = true } - findPreference.setOnPreferenceClickListener { + findPreference?.setOnPreferenceClickListener { NavigationUtil.openEqualizer(requireActivity()) true } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ImageSettingFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ImageSettingFragment.kt index 9b5873fa0..759edfa03 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ImageSettingFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ImageSettingFragment.kt @@ -17,6 +17,7 @@ package code.name.monkey.retromusic.fragments.settings import android.os.Bundle import android.view.View import androidx.preference.Preference +import code.name.monkey.retromusic.AUTO_DOWNLOAD_IMAGES_POLICY import code.name.monkey.retromusic.R /** @@ -25,7 +26,7 @@ import code.name.monkey.retromusic.R class ImageSettingFragment : AbsSettingsFragment() { override fun invalidateSettings() { - val autoDownloadImagesPolicy: Preference = findPreference("auto_download_images_policy")!! + val autoDownloadImagesPolicy: Preference = findPreference(AUTO_DOWNLOAD_IMAGES_POLICY)!! setSummary(autoDownloadImagesPolicy) autoDownloadImagesPolicy.setOnPreferenceChangeListener { _, o -> setSummary(autoDownloadImagesPolicy, o) @@ -39,7 +40,7 @@ class ImageSettingFragment : AbsSettingsFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val preference: Preference? = findPreference("auto_download_images_policy") + val preference: Preference? = findPreference(AUTO_DOWNLOAD_IMAGES_POLICY) preference?.let { setSummary(it) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NotificationSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NotificationSettingsFragment.kt index 2df2d2df6..385d3764d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NotificationSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NotificationSettingsFragment.kt @@ -21,6 +21,7 @@ import android.os.Bundle import androidx.preference.Preference import androidx.preference.TwoStatePreference import code.name.monkey.retromusic.CLASSIC_NOTIFICATION +import code.name.monkey.retromusic.COLORED_NOTIFICATION import code.name.monkey.retromusic.R import code.name.monkey.retromusic.util.PreferenceUtil @@ -34,7 +35,7 @@ class NotificationSettingsFragment : AbsSettingsFragment(), override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == CLASSIC_NOTIFICATION) { if (VERSION.SDK_INT >= VERSION_CODES.O) { - findPreference("colored_notification")?.isEnabled = + findPreference(COLORED_NOTIFICATION)?.isEnabled = sharedPreferences?.getBoolean(key, false)!! } } @@ -42,7 +43,7 @@ class NotificationSettingsFragment : AbsSettingsFragment(), override fun invalidateSettings() { - val classicNotification: TwoStatePreference? = findPreference("classic_notification") + val classicNotification: TwoStatePreference? = findPreference(CLASSIC_NOTIFICATION) if (VERSION.SDK_INT < VERSION_CODES.N) { classicNotification?.isVisible = false } else { @@ -57,7 +58,7 @@ class NotificationSettingsFragment : AbsSettingsFragment(), } } - val coloredNotification: TwoStatePreference? = findPreference("colored_notification") + val coloredNotification: TwoStatePreference? = findPreference(COLORED_NOTIFICATION) if (VERSION.SDK_INT >= VERSION_CODES.O) { coloredNotification?.isEnabled = PreferenceUtil.isClassicNotification } else { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NowPlayingSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NowPlayingSettingsFragment.kt index 24cd9dfad..f8acae350 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NowPlayingSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/NowPlayingSettingsFragment.kt @@ -33,8 +33,8 @@ class NowPlayingSettingsFragment : AbsSettingsFragment(), updateNowPlayingScreenSummary() updateAlbumCoverStyleSummary() - val carouselEffect: TwoStatePreference = findPreference("carousel_effect")!! - carouselEffect.setOnPreferenceChangeListener { _, newValue -> + val carouselEffect: TwoStatePreference? = findPreference(CAROUSEL_EFFECT) + carouselEffect?.setOnPreferenceChangeListener { _, newValue -> if (newValue as Boolean && !App.isProVersion()) { showProToastAndNavigate(getString(R.string.pref_title_toggle_carousel_effect)) return@setOnPreferenceChangeListener false @@ -60,7 +60,7 @@ class NowPlayingSettingsFragment : AbsSettingsFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) PreferenceUtil.registerOnSharedPreferenceChangedListener(this) - val preference: Preference? = findPreference("album_cover_transform") + val preference: Preference? = findPreference(ALBUM_COVER_TRANSFORM) preference?.setOnPreferenceChangeListener { albumPrefs, newValue -> setSummary(albumPrefs, newValue) true diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/OtherSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/OtherSettingsFragment.kt index c52d4adb4..b3307f932 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/OtherSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/OtherSettingsFragment.kt @@ -18,6 +18,8 @@ import android.os.Bundle import android.view.View import androidx.preference.Preference import code.name.monkey.appthemehelper.common.prefs.supportv7.ATEListPreference +import code.name.monkey.retromusic.LANGUAGE_NAME +import code.name.monkey.retromusic.LAST_ADDED_CUTOFF import code.name.monkey.retromusic.R import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.ReloadType.HomeSections @@ -29,8 +31,9 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel class OtherSettingsFragment : AbsSettingsFragment() { private val libraryViewModel by sharedViewModel() + override fun invalidateSettings() { - val languagePreference: ATEListPreference? = findPreference("language_name") + val languagePreference: ATEListPreference? = findPreference(LANGUAGE_NAME) languagePreference?.setOnPreferenceChangeListener { _, _ -> requireActivity().recreate() return@setOnPreferenceChangeListener true @@ -43,13 +46,13 @@ class OtherSettingsFragment : AbsSettingsFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val preference: Preference? = findPreference("last_added_interval") + val preference: Preference? = findPreference(LAST_ADDED_CUTOFF) preference?.setOnPreferenceChangeListener { lastAdded, newValue -> setSummary(lastAdded, newValue) libraryViewModel.forceReload(HomeSections) true } - val languagePreference: Preference? = findPreference("language_name") + val languagePreference: Preference? = findPreference(LANGUAGE_NAME) languagePreference?.setOnPreferenceChangeListener { prefs, newValue -> setSummary(prefs, newValue) true diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/PersonalizeSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/PersonalizeSettingsFragment.kt index 0a386b348..a90e6d40b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/PersonalizeSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/PersonalizeSettingsFragment.kt @@ -18,13 +18,13 @@ import android.os.Bundle import android.view.View import androidx.preference.TwoStatePreference import code.name.monkey.appthemehelper.common.prefs.supportv7.ATEListPreference -import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.* class PersonalizeSettingsFragment : AbsSettingsFragment() { override fun invalidateSettings() { - val toggleFullScreen: TwoStatePreference = findPreference("toggle_full_screen")!! - toggleFullScreen.setOnPreferenceChangeListener { _, _ -> + val toggleFullScreen: TwoStatePreference? = findPreference(TOGGLE_FULL_SCREEN) + toggleFullScreen?.setOnPreferenceChangeListener { _, _ -> requireActivity().recreate() true } @@ -36,12 +36,17 @@ class PersonalizeSettingsFragment : AbsSettingsFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val homeArtistStyle: ATEListPreference? = findPreference("home_artist_grid_style") + val homeArtistStyle: ATEListPreference? = findPreference(HOME_ARTIST_GRID_STYLE) homeArtistStyle?.setOnPreferenceChangeListener { preference, newValue -> setSummary(preference, newValue) true } - val tabTextMode: ATEListPreference? = findPreference("tab_text_mode") + val homeAlbumStyle: ATEListPreference? = findPreference(HOME_ALBUM_GRID_STYLE) + homeAlbumStyle?.setOnPreferenceChangeListener { preference, newValue -> + setSummary(preference, newValue) + true + } + val tabTextMode: ATEListPreference? = findPreference(TAB_TEXT_MODE) tabTextMode?.setOnPreferenceChangeListener { prefs, newValue -> setSummary(prefs, newValue) true diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ThemeSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ThemeSettingsFragment.kt index 7ee282619..fa8b8cc82 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ThemeSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/ThemeSettingsFragment.kt @@ -23,9 +23,7 @@ import code.name.monkey.appthemehelper.common.prefs.supportv7.ATEColorPreference import code.name.monkey.appthemehelper.common.prefs.supportv7.ATESwitchPreference import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.VersionUtils -import code.name.monkey.retromusic.App -import code.name.monkey.retromusic.DESATURATED_COLOR -import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.* import code.name.monkey.retromusic.appshortcuts.DynamicShortcutManager import code.name.monkey.retromusic.util.PreferenceUtil import com.afollestad.materialdialogs.color.ColorChooserDialog @@ -36,7 +34,7 @@ import com.afollestad.materialdialogs.color.ColorChooserDialog class ThemeSettingsFragment : AbsSettingsFragment() { override fun invalidateSettings() { - val generalTheme: Preference? = findPreference("general_theme") + val generalTheme: Preference? = findPreference(GENERAL_THEME) generalTheme?.let { setSummary(it) it.setOnPreferenceChangeListener { _, newValue -> @@ -53,11 +51,10 @@ class ThemeSettingsFragment : AbsSettingsFragment() { } } - val accentColorPref: ATEColorPreference = findPreference("accent_color")!! + val accentColorPref: ATEColorPreference? = findPreference(ACCENT_COLOR) val accentColor = ThemeStore.accentColor(requireContext()) - accentColorPref.setColor(accentColor, ColorUtil.darkenColor(accentColor)) - - accentColorPref.setOnPreferenceClickListener { + accentColorPref?.setColor(accentColor, ColorUtil.darkenColor(accentColor)) + accentColorPref?.setOnPreferenceClickListener { ColorChooserDialog.Builder(requireContext(), R.string.accent_color) .accentMode(true) .allowUserColorInput(true) @@ -66,7 +63,7 @@ class ThemeSettingsFragment : AbsSettingsFragment() { .show(requireActivity()) return@setOnPreferenceClickListener true } - val blackTheme: ATESwitchPreference? = findPreference("black_theme") + val blackTheme: ATESwitchPreference? = findPreference(BLACK_THEME) blackTheme?.setOnPreferenceChangeListener { _, _ -> if (!App.isProVersion()) { showProToastAndNavigate("Just Black theme") @@ -94,12 +91,12 @@ class ThemeSettingsFragment : AbsSettingsFragment() { } - val colorAppShortcuts: TwoStatePreference = findPreference("should_color_app_shortcuts")!! + val colorAppShortcuts: TwoStatePreference? = findPreference(SHOULD_COLOR_APP_SHORTCUTS) if (!VersionUtils.hasNougatMR()) { - colorAppShortcuts.isVisible = false + colorAppShortcuts?.isVisible = false } else { - colorAppShortcuts.isChecked = PreferenceUtil.isColoredAppShortcuts - colorAppShortcuts.setOnPreferenceChangeListener { _, newValue -> + colorAppShortcuts?.isChecked = PreferenceUtil.isColoredAppShortcuts + colorAppShortcuts?.setOnPreferenceChangeListener { _, newValue -> PreferenceUtil.isColoredAppShortcuts = newValue as Boolean DynamicShortcutManager(requireContext()).updateDynamicShortcuts() true diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt index 22221639e..87befa582 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt @@ -397,12 +397,23 @@ object PreferenceUtil { } } - val homeGridStyle: Int + val homeArtistGridStyle: Int get() { - val position = - sharedPreferences.getStringOrDefault( - HOME_ARTIST_GRID_STYLE, "0" - ).toInt() + val position = sharedPreferences.getStringOrDefault( + HOME_ARTIST_GRID_STYLE, "0" + ).toInt() + val typedArray = + App.getContext().resources.obtainTypedArray(R.array.pref_home_grid_style_layout) + val layoutRes = typedArray.getResourceId(position, 0) + typedArray.recycle() + return if (layoutRes == 0) { + R.layout.item_artist + } else layoutRes + } + + val homeAlbumGridStyle: Int + get() { + val position = sharedPreferences.getStringOrDefault(HOME_ALBUM_GRID_STYLE, "0").toInt() val typedArray = App.getContext().resources.obtainTypedArray(R.array.pref_home_grid_style_layout) val layoutRes = typedArray.getResourceId(position, 0) diff --git a/app/src/main/res/layout-land/fragment_album_details.xml b/app/src/main/res/layout-land/fragment_album_details.xml index 3bae2be7f..375521e79 100644 --- a/app/src/main/res/layout-land/fragment_album_details.xml +++ b/app/src/main/res/layout-land/fragment_album_details.xml @@ -68,7 +68,9 @@ android:id="@+id/container" android:layout_width="0dp" android:layout_height="0dp" + android:descendantFocusability="beforeDescendants" android:fillViewport="true" + android:focusableInTouchMode="true" android:overScrollMode="never" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-land/fragment_artist_details.xml b/app/src/main/res/layout-land/fragment_artist_details.xml index 2a0cae060..ba8a9f5a8 100644 --- a/app/src/main/res/layout-land/fragment_artist_details.xml +++ b/app/src/main/res/layout-land/fragment_artist_details.xml @@ -69,6 +69,8 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_weight="1" + android:descendantFocusability="beforeDescendants" + android:focusableInTouchMode="true" android:overScrollMode="never" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout-land/fragment_banner_home.xml b/app/src/main/res/layout-land/fragment_banner_home.xml index 97e02abba..83422bb38 100644 --- a/app/src/main/res/layout-land/fragment_banner_home.xml +++ b/app/src/main/res/layout-land/fragment_banner_home.xml @@ -16,6 +16,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" + android:descendantFocusability="beforeDescendants" + android:focusableInTouchMode="true" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="@dimen/toolbar_margin_horizontal" diff --git a/app/src/main/res/layout-land/fragment_home.xml b/app/src/main/res/layout-land/fragment_home.xml index 5e50ad756..a2e570c37 100644 --- a/app/src/main/res/layout-land/fragment_home.xml +++ b/app/src/main/res/layout-land/fragment_home.xml @@ -20,6 +20,8 @@ android:layout_height="match_parent" android:layout_marginStart="@dimen/toolbar_margin_horizontal" android:layout_marginEnd="@dimen/toolbar_margin_horizontal" + android:descendantFocusability="beforeDescendants" + android:focusableInTouchMode="true" android:overScrollMode="never" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> diff --git a/app/src/main/res/layout/fragment_album_details.xml b/app/src/main/res/layout/fragment_album_details.xml index 2ad7e45b0..bb2ac3490 100644 --- a/app/src/main/res/layout/fragment_album_details.xml +++ b/app/src/main/res/layout/fragment_album_details.xml @@ -32,6 +32,8 @@ diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index e9937fbe9..1233a2a5e 100755 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -17,6 +17,8 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" + android:descendantFocusability="beforeDescendants" + android:focusableInTouchMode="true" android:overScrollMode="never" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> diff --git a/app/src/main/res/master/values-af-rZA/strings.xml b/app/src/main/res/master/values-af-rZA/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-af-rZA/strings.xml +++ b/app/src/main/res/master/values-af-rZA/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-bn-rIN/strings.xml b/app/src/main/res/master/values-bn-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-bn-rIN/strings.xml +++ b/app/src/main/res/master/values-bn-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ca-rES/strings.xml b/app/src/main/res/master/values-ca-rES/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-ca-rES/strings.xml +++ b/app/src/main/res/master/values-ca-rES/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-cs-rCZ/strings.xml b/app/src/main/res/master/values-cs-rCZ/strings.xml index 7d8a1c7ec..0586557fb 100644 --- a/app/src/main/res/master/values-cs-rCZ/strings.xml +++ b/app/src/main/res/master/values-cs-rCZ/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Může způsobit problémy s přehráváním u některých zařízení." Toggle genre tab - Toggle home banner style + Show or hide the home banner Může zvýšit kvalitu obalu alba, ale způsobí pomalejší načítání snímků. Tuto možnost povolte pouze v případě potíží s uměleckými díly s nízkým rozlišením. Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Přehrávání bez mezery Hlavní téma Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignorovat obaly v zařízení Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-da-rDK/strings.xml b/app/src/main/res/master/values-da-rDK/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-da-rDK/strings.xml +++ b/app/src/main/res/master/values-da-rDK/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-el-rGR/strings.xml b/app/src/main/res/master/values-el-rGR/strings.xml index 207851a8a..b68f5bc70 100644 --- a/app/src/main/res/master/values-el-rGR/strings.xml +++ b/app/src/main/res/master/values-el-rGR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Μπορεί να προκαλέσει προβλήματα με την αναπαραγωγή σε κάποιες συσκευές." Toggle genre tab - Toggle home banner style + Show or hide the home banner Μπορεί να αυξήσει την ποιότητα των εξωφύλλων άλμπουμ, αλλά προκαλεί αργή φόρτωση εικόνων. Ενεργοποιήστε αυτή την επίλογη μόνο εαν αντιμετωπίζετε προβλήματα με εξώφυλλα χαμηλής ανάλυσης. Configure visibility and order of library categories. Ενεργοποίηση διακοπτών ρύθμισης στην οθόνη κλειδώματος. @@ -345,8 +345,8 @@ Αναπαραγωγή χωρίς κενά Γενικό θέμα Show genre tab - Home artist grid - Home banner + Artist grid + Banner Παράληψη Media Store για εξώφυλλα Χρονικό διάστημα playlist \"Προστέθηκε τελευταία\" Full screen Ρυθμίσεις diff --git a/app/src/main/res/master/values-en-rUS/strings.xml b/app/src/main/res/master/values-en-rUS/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-en-rUS/strings.xml +++ b/app/src/main/res/master/values-en-rUS/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-fa-rIR/strings.xml b/app/src/main/res/master/values-fa-rIR/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-fa-rIR/strings.xml +++ b/app/src/main/res/master/values-fa-rIR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-fi-rFI/strings.xml b/app/src/main/res/master/values-fi-rFI/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-fi-rFI/strings.xml +++ b/app/src/main/res/master/values-fi-rFI/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-hi-rIN/strings.xml b/app/src/main/res/master/values-hi-rIN/strings.xml index 9e11f38ee..56dbeb11c 100644 --- a/app/src/main/res/master/values-hi-rIN/strings.xml +++ b/app/src/main/res/master/values-hi-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-it-rIT/strings.xml b/app/src/main/res/master/values-it-rIT/strings.xml index 2d844245b..3ab5f6486 100644 --- a/app/src/main/res/master/values-it-rIT/strings.xml +++ b/app/src/main/res/master/values-it-rIT/strings.xml @@ -347,7 +347,7 @@ https://play.google.com/store/apps/details?id=%s Tema generale Mostra scheda Genere Griglia schermata artista - Home banner + Banner Ignora le copertine del Media Store Intervallo playlist ultimi aggiunti Controlli a schermo intero diff --git a/app/src/main/res/master/values-iw-rIL/strings.xml b/app/src/main/res/master/values-iw-rIL/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-iw-rIL/strings.xml +++ b/app/src/main/res/master/values-iw-rIL/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-kn-rIN/strings.xml b/app/src/main/res/master/values-kn-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-kn-rIN/strings.xml +++ b/app/src/main/res/master/values-kn-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ko-rKR/strings.xml b/app/src/main/res/master/values-ko-rKR/strings.xml index 8d916b7c0..10c27f2e8 100644 --- a/app/src/main/res/master/values-ko-rKR/strings.xml +++ b/app/src/main/res/master/values-ko-rKR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "몇몇 기기에서 재생 문제를 유발할 수 있습니다." Toggle genre tab - Toggle home banner style + Show or hide the home banner 앨범 커버의 품질을 향상시킬 수 있지만 이미지를 불러오는 시간이 늘어납니다. 저해상도 이미지를 불러오는 데 문제가 있는 경우에만 사용하십시오. Configure visibility and order of library categories. Retro music에서 제공하는 자체 잠금 화면 사용 @@ -345,8 +345,8 @@ 지연없이 재생하기 기본 테마 Show genre tab - Home artist grid - Home banner + Artist grid + Banner 미디어 저장소 커버 무시 최근 추가된 음악 간격 지정 전체 화면 컨트롤 diff --git a/app/src/main/res/master/values-ml-rIN/strings.xml b/app/src/main/res/master/values-ml-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-ml-rIN/strings.xml +++ b/app/src/main/res/master/values-ml-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ne-rIN/strings.xml b/app/src/main/res/master/values-ne-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-ne-rIN/strings.xml +++ b/app/src/main/res/master/values-ne-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-nl-rNL/strings.xml b/app/src/main/res/master/values-nl-rNL/strings.xml index 1fb659148..710fef05f 100644 --- a/app/src/main/res/master/values-nl-rNL/strings.xml +++ b/app/src/main/res/master/values-nl-rNL/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Kan afspeelproblemen veroorzaken op sommige toestellen" Toggle genre tab - Toggle home banner style + Show or hide the home banner Kan album cover kwaliteit verbeteren, maar veroorzaakt langere laadtijden. Alleen aanzetten als je problemen hebt met lage resolutie artworks Configure visibility and order of library categories. Zet besturing knoppen aan op vergrendelscherm @@ -345,8 +345,8 @@ Afspelen zonder pauzes Basis thema Show genre tab - Home artist grid - Home banner + Artist grid + Banner Negeer media store covers Laatst toegevoegde afspeellijst interval Volledig scherm besturing knoppen diff --git a/app/src/main/res/master/values-no-rNO/strings.xml b/app/src/main/res/master/values-no-rNO/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-no-rNO/strings.xml +++ b/app/src/main/res/master/values-no-rNO/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-or-rIN/strings.xml b/app/src/main/res/master/values-or-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-or-rIN/strings.xml +++ b/app/src/main/res/master/values-or-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-pt-rPT/strings.xml b/app/src/main/res/master/values-pt-rPT/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-pt-rPT/strings.xml +++ b/app/src/main/res/master/values-pt-rPT/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ro-rRO/strings.xml b/app/src/main/res/master/values-ro-rRO/strings.xml index c68d25e96..c971ce2a0 100644 --- a/app/src/main/res/master/values-ro-rRO/strings.xml +++ b/app/src/main/res/master/values-ro-rRO/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Poate cauza probleme de redare pe unele dispozitive." Toggle genre tab - Toggle home banner style + Show or hide the home banner Poate mări calitatea copertei de album, dar cauzează încărcarea mai lentă a imaginilor. Activați această opțiune doar dacă aveți probleme cu coperta de album cu rezoluție mică. Configure visibility and order of library categories. Comenzi pe ecranul de blocare pentru Retro music. @@ -345,8 +345,8 @@ Redare \"Gapless\" Temă Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignoră copertele de pe Magazinul Media Ultimul interval de playlist adăugat Comenzi ecran complet diff --git a/app/src/main/res/master/values-sk-rSK/strings.xml b/app/src/main/res/master/values-sk-rSK/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-sk-rSK/strings.xml +++ b/app/src/main/res/master/values-sk-rSK/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-sr-rSP/strings.xml b/app/src/main/res/master/values-sr-rSP/strings.xml index fcc543df6..a0d473c74 100644 --- a/app/src/main/res/master/values-sr-rSP/strings.xml +++ b/app/src/main/res/master/values-sr-rSP/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Moze prouzrokovati probleme na pojedinim uredjajima." Toggle genre tab - Toggle home banner style + Show or hide the home banner oze poboljsati kvalitet omota albuma ali uzrokuje njegovo sporije ucitavanje. Omoguci ovo samo ukoliko imas problema sa losim kvalitetom slike omota albuma. Configure visibility and order of library categories. Prikazuj kontrole na zakljucanom ekranu @@ -345,8 +345,8 @@ Neuznemiravano reprodukovanje Opsta tema Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignorisi omote sa prodavnice Interval plejliste poslednje dodato Kontrole preko celog ekrana diff --git a/app/src/main/res/master/values-sv-rSE/strings.xml b/app/src/main/res/master/values-sv-rSE/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-sv-rSE/strings.xml +++ b/app/src/main/res/master/values-sv-rSE/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ta-rIN/strings.xml b/app/src/main/res/master/values-ta-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-ta-rIN/strings.xml +++ b/app/src/main/res/master/values-ta-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-ur-rIN/strings.xml b/app/src/main/res/master/values-ur-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/master/values-ur-rIN/strings.xml +++ b/app/src/main/res/master/values-ur-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/master/values-zh-rTW/strings.xml b/app/src/main/res/master/values-zh-rTW/strings.xml index 4fca6fcf7..fa860ba65 100644 --- a/app/src/main/res/master/values-zh-rTW/strings.xml +++ b/app/src/main/res/master/values-zh-rTW/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "可能會在某些裝置上出現播放問題。" Toggle genre tab - Toggle home banner style + Show or hide the home banner 提高專輯封面的成像品質,但會造成較長的讀取時間。建議只有在您對低畫質的專輯封面有問題時才開啟此選項。 Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ 無縫播放 主題 Show genre tab - Home artist grid - Home banner + Artist grid + Banner 忽略音訊檔內嵌的專輯封面 Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 03342c322..cd2992854 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Může způsobit problémy s přehráváním u některých zařízení." Toggle genre tab - Toggle home banner style + Show or hide the home banner Může zvýšit kvalitu obalu alba, ale způsobí pomalejší načítání snímků. Tuto možnost povolte pouze v případě potíží s uměleckými díly s nízkým rozlišením. Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Přehrávání bez mezery Hlavní téma Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignorovat obaly v zařízení Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 41fcf970b..8a4f6068a 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Μπορεί να προκαλέσει προβλήματα με την αναπαραγωγή σε κάποιες συσκευές." Toggle genre tab - Toggle home banner style + Show or hide the home banner Μπορεί να αυξήσει την ποιότητα των εξωφύλλων άλμπουμ, αλλά προκαλεί αργή φόρτωση εικόνων. Ενεργοποιήστε αυτή την επίλογη μόνο εαν αντιμετωπίζετε προβλήματα με εξώφυλλα χαμηλής ανάλυσης. Configure visibility and order of library categories. Ενεργοποίηση διακοπτών ρύθμισης στην οθόνη κλειδώματος. @@ -345,8 +345,8 @@ Αναπαραγωγή χωρίς κενά Γενικό θέμα Show genre tab - Home artist grid - Home banner + Artist grid + Banner Παράληψη Media Store για εξώφυλλα Χρονικό διάστημα playlist \"Προστέθηκε τελευταία\" Full screen Ρυθμίσεις diff --git a/app/src/main/res/values-en-rHK/strings.xml b/app/src/main/res/values-en-rHK/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/values-en-rHK/strings.xml +++ b/app/src/main/res/values-en-rHK/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-en-rID/strings.xml b/app/src/main/res/values-en-rID/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/values-en-rID/strings.xml +++ b/app/src/main/res/values-en-rID/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-en-rIN/strings.xml b/app/src/main/res/values-en-rIN/strings.xml index 610335234..1bb735e39 100644 --- a/app/src/main/res/values-en-rIN/strings.xml +++ b/app/src/main/res/values-en-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-en-rUS/strings.xml +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 6913810e8..c1622e83e 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 56287e50d..289384035 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -347,7 +347,7 @@ https://play.google.com/store/apps/details?id=%s Tema generale Mostra scheda Genere Griglia schermata artista - Home banner + Banner Ignora le copertine del Media Store Intervallo playlist ultimi aggiunti Controlli a schermo intero diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index d6a358d48..c432a0a58 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 84dd888cb..bf3a4d17e 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "몇몇 기기에서 재생 문제를 유발할 수 있습니다." Toggle genre tab - Toggle home banner style + Show or hide the home banner 앨범 커버의 품질을 향상시킬 수 있지만 이미지를 불러오는 시간이 늘어납니다. 저해상도 이미지를 불러오는 데 문제가 있는 경우에만 사용하십시오. Configure visibility and order of library categories. Retro music에서 제공하는 자체 잠금 화면 사용 @@ -345,8 +345,8 @@ 지연없이 재생하기 기본 테마 Show genre tab - Home artist grid - Home banner + Artist grid + Banner 미디어 저장소 커버 무시 최근 추가된 음악 간격 지정 전체 화면 컨트롤 diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-ne-rIN/strings.xml b/app/src/main/res/values-ne-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-ne-rIN/strings.xml +++ b/app/src/main/res/values-ne-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 87013f4dd..de751b869 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Kan afspeelproblemen veroorzaken op sommige toestellen" Toggle genre tab - Toggle home banner style + Show or hide the home banner Kan album cover kwaliteit verbeteren, maar veroorzaakt langere laadtijden. Alleen aanzetten als je problemen hebt met lage resolutie artworks Configure visibility and order of library categories. Zet besturing knoppen aan op vergrendelscherm @@ -345,8 +345,8 @@ Afspelen zonder pauzes Basis thema Show genre tab - Home artist grid - Home banner + Artist grid + Banner Negeer media store covers Laatst toegevoegde afspeellijst interval Volledig scherm besturing knoppen diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index ffefc5eca..6ac56419f 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Poate cauza probleme de redare pe unele dispozitive." Toggle genre tab - Toggle home banner style + Show or hide the home banner Poate mări calitatea copertei de album, dar cauzează încărcarea mai lentă a imaginilor. Activați această opțiune doar dacă aveți probleme cu coperta de album cu rezoluție mică. Configure visibility and order of library categories. Comenzi pe ecranul de blocare pentru Retro music. @@ -345,8 +345,8 @@ Redare \"Gapless\" Temă Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignoră copertele de pe Magazinul Media Ultimul interval de playlist adăugat Comenzi ecran complet diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 346fa1647..ed335f517 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 62204deab..3fb8f47e4 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Moze prouzrokovati probleme na pojedinim uredjajima." Toggle genre tab - Toggle home banner style + Show or hide the home banner oze poboljsati kvalitet omota albuma ali uzrokuje njegovo sporije ucitavanje. Omoguci ovo samo ukoliko imas problema sa losim kvalitetom slike omota albuma. Configure visibility and order of library categories. Prikazuj kontrole na zakljucanom ekranu @@ -345,8 +345,8 @@ Neuznemiravano reprodukovanje Opsta tema Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignorisi omote sa prodavnice Interval plejliste poslednje dodato Kontrole preko celog ekrana diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-ur-rIN/strings.xml b/app/src/main/res/values-ur-rIN/strings.xml index 12d03622e..d2b1c3ba9 100644 --- a/app/src/main/res/values-ur-rIN/strings.xml +++ b/app/src/main/res/values-ur-rIN/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9639c9ecf..a5a736ce3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -313,7 +313,7 @@ Show extra Song information, such as file format, bitrate and frequency "可能會在某些裝置上出現播放問題。" Toggle genre tab - Toggle home banner style + Show or hide the home banner 提高專輯封面的成像品質,但會造成較長的讀取時間。建議只有在您對低畫質的專輯封面有問題時才開啟此選項。 Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -345,8 +345,8 @@ 無縫播放 主題 Show genre tab - Home artist grid - Home banner + Artist grid + Banner 忽略音訊檔內嵌的專輯封面 Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be213e122..c72762535 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -537,7 +537,7 @@ Show extra Song information, such as file format, bitrate and frequency "Can cause playback issues on some devices." Toggle genre tab - Toggle home banner style + Show or hide the home banner Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks Configure visibility and order of library categories. Use Retro Music\'s custom lockscreen controls @@ -571,8 +571,9 @@ Gapless playback App theme Show genre tab - Home artist grid - Home banner + Artist grid + Album grid + Banner Ignore Media Store covers Last added playlist interval Fullscreen controls diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 01529cc06..c6c5be0ac 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,18 +6,21 @@ @color/window_color_light none 16dp + @style/WindowAnimationTransition + + diff --git a/app/src/main/res/xml/pref_ui.xml b/app/src/main/res/xml/pref_ui.xml index 50c1e7d29..27cac1c8b 100644 --- a/app/src/main/res/xml/pref_ui.xml +++ b/app/src/main/res/xml/pref_ui.xml @@ -16,6 +16,17 @@ android:title="@string/pref_title_home_artist_grid_style" app:icon="@drawable/ic_home" /> + + Date: Fri, 25 Sep 2020 02:33:56 +0530 Subject: [PATCH 23/72] Dynamic navigation --- app/build.gradle | 2 +- .../retromusic/extensions/ViewExtensions.kt | 3 +-- .../fragments/base/AbsMusicServiceFragment.kt | 3 --- .../fragments/library/LibraryFragment.kt | 20 +++++++++++++++---- .../monkey/retromusic/model/CategoryInfo.kt | 1 - app/src/main/res/layout/fragment_library.xml | 6 ++---- app/src/main/res/xml/pref_advanced.xml | 9 --------- 7 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 30e723710..44d5b4178 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { } signingConfigs { release { - Properties properties = getProperties('/Users/apple/Documents/Github/retro.properties ') + Properties properties = getProperties('C:/Users/h4h13/Documents/GitHub/retro.properties ') storeFile file(getProperty(properties, 'storeFile')) keyAlias getProperty(properties, 'keyAlias') storePassword getProperty(properties, 'storePassword') diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 42c0d1d4c..1e78b6902 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -51,8 +51,7 @@ fun EditText.appHandleColor(): EditText { } -fun View.translateXAnimate(value: Float) { - println("translateXAnimate $value") +fun View.translateXAnimate(value: Float) { ObjectAnimator.ofFloat(this, "translationY", value) .apply { duration = 300 diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt index fcfdc3c25..e88000aff 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMusicServiceFragment.kt @@ -27,9 +27,6 @@ open class AbsMusicServiceFragment(@LayoutRes layout: Int) : Fragment(layout), val navOptions by lazy { navOptions { - popUpTo(R.id.action_home) { - inclusive = false - } launchSingleTop = false anim { enter = R.anim.retro_fragment_open_enter diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 1b2144aa1..10a1630e2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -5,6 +5,7 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import androidx.core.text.HtmlCompat +import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import androidx.navigation.ui.NavigationUI import code.name.monkey.appthemehelper.ThemeStore @@ -13,10 +14,12 @@ import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.R import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog -import code.name.monkey.retromusic.extensions.findNavController +import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment +import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.state.NowPlayingPanelState +import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.android.synthetic.main.fragment_library.* import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -53,10 +56,19 @@ class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { } private fun setupNavigationController() { - val navController = findNavController(R.id.fragment_container) + val navHostFragment = whichFragment(R.id.fragment_container) + val navController = navHostFragment.navController + val navInflater = navController.navInflater + val navGraph = navInflater.inflate(R.navigation.library_graph) + + val categoryInfo: CategoryInfo = PreferenceUtil.libraryCategory.first { it.visible } + if (categoryInfo.visible) { + navGraph.startDestination = categoryInfo.category.id + } + navController.graph = navGraph NavigationUI.setupWithNavController(mainActivity.getBottomNavigationView(), navController) - navController.addOnDestinationChangedListener { controller, destination, arguments -> - appBarLayout.setExpanded(true,true) + navController.addOnDestinationChangedListener { _, _, _ -> + appBarLayout.setExpanded(true, true) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/CategoryInfo.kt b/app/src/main/java/code/name/monkey/retromusic/model/CategoryInfo.kt index 978c2fe1b..e11eb0504 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/CategoryInfo.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/CategoryInfo.kt @@ -39,5 +39,4 @@ data class CategoryInfo( Genres(R.id.action_genre, R.string.genres, R.drawable.ic_guitar), Folder(R.id.action_folder, R.string.folders, R.drawable.ic_folder); } - } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 59a58832b..67663cbe3 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -28,10 +28,10 @@ tools:ignore="UnusedAttribute"> @@ -48,7 +48,5 @@ android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" - app:defaultNavHost="true" - app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" - app:navGraph="@navigation/library_graph" /> + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_advanced.xml b/app/src/main/res/xml/pref_advanced.xml index d1f82c7f9..b993244d9 100755 --- a/app/src/main/res/xml/pref_advanced.xml +++ b/app/src/main/res/xml/pref_advanced.xml @@ -59,15 +59,6 @@ android:title="@string/pref_keep_screen_on_title" app:icon="@drawable/ic_settings_brigntness" /> - Date: Fri, 25 Sep 2020 02:47:51 +0530 Subject: [PATCH 24/72] Update build.gradle --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 44d5b4178..30e723710 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { } signingConfigs { release { - Properties properties = getProperties('C:/Users/h4h13/Documents/GitHub/retro.properties ') + Properties properties = getProperties('/Users/apple/Documents/Github/retro.properties ') storeFile file(getProperty(properties, 'storeFile')) keyAlias getProperty(properties, 'keyAlias') storePassword getProperty(properties, 'storePassword') From 46c65a7ebc07f730d5913438206c06c8e1ac6ff0 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 25 Sep 2020 03:11:34 +0530 Subject: [PATCH 25/72] Details removed extra space --- app/build.gradle | 6 +++--- .../main/res/layout-land/fragment_album_details.xml | 12 +----------- .../main/res/layout-land/fragment_artist_details.xml | 12 +----------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 30e723710..462a02d14 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10443 - versionName '3.6.000' + "_" + getDate() + versionCode 10445 + versionName '3.6.100' + "_" + getDate() multiDexEnabled true @@ -34,7 +34,7 @@ android { } buildTypes { release { - debuggable true + //debuggable true minifyEnabled true //shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' diff --git a/app/src/main/res/layout-land/fragment_album_details.xml b/app/src/main/res/layout-land/fragment_album_details.xml index 375521e79..40e2d12d8 100644 --- a/app/src/main/res/layout-land/fragment_album_details.xml +++ b/app/src/main/res/layout-land/fragment_album_details.xml @@ -9,16 +9,6 @@ android:transitionName="@string/transition_album_art" tools:ignore="UnusedAttribute"> - - - - + app:layout_constraintTop_toTopOf="parent"> - - - - + app:layout_constraintTop_toTopOf="parent"> Date: Fri, 25 Sep 2020 23:08:59 +0530 Subject: [PATCH 26/72] I belived fixed android Navigation --- .../retromusic/activities/MainActivity.kt | 21 ++ .../fragments/albums/AlbumsFragment.kt | 3 +- .../fragments/artists/ArtistsFragment.kt | 4 +- .../fragments/base/AbsRecyclerViewFragment.kt | 73 +++++- .../fragments/folder/FoldersFragment.java | 39 ++++ .../retromusic/fragments/home/HomeFragment.kt | 60 ++++- .../fragments/playlists/PlaylistsFragment.kt | 3 +- .../fragments/songs/SongsFragment.kt | 2 +- .../main/res/layout/activity_main_content.xml | 3 +- .../main/res/layout/fragment_banner_home.xml | 220 +++++++++++------- app/src/main/res/layout/fragment_folder.xml | 133 +++++++---- app/src/main/res/layout/fragment_home.xml | 148 +++++++----- .../res/layout/fragment_main_recycler.xml | 92 ++++++++ app/src/main/res/navigation/main_graph.xml | 41 +++- 14 files changed, 639 insertions(+), 203 deletions(-) create mode 100644 app/src/main/res/layout/fragment_main_recycler.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index 6eb4cc738..2d4023d93 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -8,11 +8,13 @@ import android.os.Bundle import android.provider.MediaStore import android.view.View import androidx.lifecycle.lifecycleScope +import androidx.navigation.ui.NavigationUI import code.name.monkey.retromusic.* import code.name.monkey.retromusic.activities.base.AbsSlidingMusicPanelActivity import code.name.monkey.retromusic.extensions.findNavController import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.SearchQueryHelper.getSongs +import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.PlaylistSongsLoader import code.name.monkey.retromusic.service.MusicService @@ -45,6 +47,25 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis hideStatusBar() AppRater.appLaunched(this) updateTabs() + + //NavigationUI.setupWithNavController(getBottomNavigationView(), findNavController(R.id.fragment_container)) + setupNavigationController() + } + + private fun setupNavigationController() { + val navController = findNavController(R.id.fragment_container) + val navInflater = navController.navInflater + val navGraph = navInflater.inflate(R.navigation.main_graph) + + val categoryInfo: CategoryInfo = PreferenceUtil.libraryCategory.first { it.visible } + if (categoryInfo.visible) { + navGraph.startDestination = categoryInfo.category.id + } + navController.graph = navGraph + NavigationUI.setupWithNavController(getBottomNavigationView(), navController) + navController.addOnDestinationChangedListener { _, _, _ -> + //appBarLayout.setExpanded(true, true) + } } override fun onSupportNavigateUp(): Boolean = diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt index d3ff1601d..96c5b9d81 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt @@ -16,7 +16,6 @@ import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.SortOrder.AlbumSortOrder import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil -import com.google.android.material.transition.platform.MaterialFadeThrough class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), @@ -110,6 +109,7 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), @@ -102,6 +101,7 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment, LM : RecyclerView.LayoutManager> : - AbsMusicServiceFragment(R.layout.fragment_main_activity_recycler_view), + AbsMainActivityFragment(R.layout.fragment_main_recycler), AppBarLayout.OnOffsetChangedListener { val libraryViewModel: LibraryViewModel by sharedViewModel() @@ -36,9 +43,30 @@ abstract class AbsRecyclerViewFragment, LM : Recycle override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) + mainActivity.setSupportActionBar(toolbar) + mainActivity.supportActionBar?.title = null initLayoutManager() initAdapter() setUpRecyclerView() + setupTitle() + } + + private fun setupTitle() { + toolbar.setNavigationOnClickListener { + findNavController().navigate( + R.id.searchFragment, + null, + navOptions + ) + } + val color = ThemeStore.accentColor(requireContext()) + val hexColor = String.format("#%06X", 0xFFFFFF and color) + val appName = HtmlCompat.fromHtml( + "Retro Music", + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + appNameText.text = appName } private fun setUpRecyclerView() { @@ -78,7 +106,7 @@ abstract class AbsRecyclerViewFragment, LM : Recycle return String(Character.toChars(unicode)) } - private fun checkIsEmpty() { + private fun checkIsEmpty() { emptyText.setText(emptyMessage) empty.visibility = if (adapter!!.itemCount == 0) View.VISIBLE else View.GONE } @@ -138,4 +166,39 @@ abstract class AbsRecyclerViewFragment, LM : Recycle fun recyclerView(): RecyclerView { return recyclerView } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_main, menu) + ToolbarContentTintHelper.handleOnCreateOptionsMenu( + requireContext(), + toolbar, + menu, + ATHToolbarActivity.getToolbarBackgroundColor(toolbar) + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_settings -> findNavController().navigate( + R.id.settingsActivity, + null, + navOptions + ) + R.id.action_import_playlist -> ImportPlaylistDialog().show( + childFragmentManager, + "ImportPlaylist" + ) + R.id.action_add_to_playlist -> CreatePlaylistDialog.create(emptyList()).show( + childFragmentManager, + "ShowCreatePlaylistDialog" + ) + } + return super.onOptionsItemSelected(item) + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index 329172b7f..ad0a4b7a0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -20,6 +20,7 @@ import android.media.MediaScannerConnection; import android.os.Bundle; import android.os.Environment; import android.text.Html; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -34,8 +35,11 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; +import androidx.core.text.HtmlCompat; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; +import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -57,6 +61,7 @@ import java.util.List; import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; +import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.adapter.SongFileAdapter; import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment; @@ -79,6 +84,8 @@ import code.name.monkey.retromusic.views.BreadCrumbLayout; import code.name.monkey.retromusic.views.ScrollingViewOnApplyWindowInsetsListener; import me.zhanghai.android.fastscroll.FastScroller; +import static code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor; + public class FoldersFragment extends AbsMainActivityFragment implements IMainActivityFragmentCallbacks, ICabHolder, @@ -95,6 +102,8 @@ public class FoldersFragment extends AbsMainActivityFragment implements private static final String CRUMBS = "crumbs"; private static final int LOADER_ID = 5; private SongFileAdapter adapter; + private Toolbar toolbar; + private TextView appNameText; private BreadCrumbLayout breadCrumbs; private MaterialCab cab; private View coordinatorLayout; @@ -154,11 +163,27 @@ public class FoldersFragment extends AbsMainActivityFragment implements @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getMainActivity().setSupportActionBar(toolbar); + getMainActivity().getSupportActionBar().setTitle(null); setStatusBarColorAuto(view); setUpAppbarColor(); setUpBreadCrumbs(); setUpRecyclerView(); setUpAdapter(); + setUpTitle(); + } + + private void setUpTitle() { + toolbar.setNavigationOnClickListener(v -> + Navigation.findNavController(v).navigate(R.id.searchFragment, null, getNavOptions()) + ); + int color = ThemeStore.Companion.accentColor(requireContext()); + String hexColor = String.format("#%06X", 0xFFFFFF & color); + Spanned appName = HtmlCompat.fromHtml( + "Retro Music", + HtmlCompat.FROM_HTML_MODE_COMPACT + ); + appNameText.setText(appName); } @Override @@ -329,6 +354,12 @@ public class FoldersFragment extends AbsMainActivityFragment implements .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); } + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar); + } + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -337,6 +368,12 @@ public class FoldersFragment extends AbsMainActivityFragment implements menu.removeItem(R.id.action_grid_size); menu.removeItem(R.id.action_layout_type); menu.removeItem(R.id.action_sort_order); + ToolbarContentTintHelper.handleOnCreateOptionsMenu( + requireContext(), + toolbar, + menu, + getToolbarBackgroundColor(toolbar) + ); } @Override @@ -420,6 +457,8 @@ public class FoldersFragment extends AbsMainActivityFragment implements breadCrumbs = view.findViewById(R.id.breadCrumbs); empty = view.findViewById(android.R.id.empty); emojiText = view.findViewById(R.id.emptyEmoji); + toolbar = view.findViewById(R.id.toolbar); + appNameText = view.findViewById(R.id.appNameText); } private void saveScrollPosition() { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index 60f5b7074..905c30e4b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -18,16 +18,24 @@ import android.app.ActivityOptions import android.os.Bundle import android.view.Menu import android.view.MenuInflater +import android.view.MenuItem import android.view.MenuItem.SHOW_AS_ACTION_IF_ROOM import android.view.View import androidx.core.os.bundleOf +import androidx.core.text.HtmlCompat import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager +import code.name.monkey.appthemehelper.ThemeStore +import code.name.monkey.appthemehelper.common.ATHToolbarActivity +import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.HISTORY_PLAYLIST import code.name.monkey.retromusic.LAST_ADDED_PLAYLIST import code.name.monkey.retromusic.R import code.name.monkey.retromusic.TOP_PLAYED_PLAYLIST import code.name.monkey.retromusic.adapter.HomeAdapter +import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog +import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment @@ -36,7 +44,6 @@ import code.name.monkey.retromusic.glide.UserProfileGlideRequest import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil import com.bumptech.glide.Glide -import com.google.android.material.transition.platform.MaterialFadeThrough import kotlinx.android.synthetic.main.abs_playlists.* import kotlinx.android.synthetic.main.fragment_banner_home.* import kotlinx.android.synthetic.main.home_content.* @@ -48,6 +55,8 @@ class HomeFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + mainActivity.setSupportActionBar(toolbar) + mainActivity.supportActionBar?.title = null setStatusBarColorAuto(view) bannerImage?.setOnClickListener { val options = ActivityOptions.makeSceneTransitionAnimation( @@ -104,6 +113,24 @@ class HomeFragment : }) loadProfile() + setupTitle() + } + + private fun setupTitle() { + toolbar.setNavigationOnClickListener { + findNavController().navigate( + R.id.searchFragment, + null, + navOptions + ) + } + val color = ThemeStore.accentColor(requireContext()) + val hexColor = String.format("#%06X", 0xFFFFFF and color) + val appName = HtmlCompat.fromHtml( + "Retro Music", + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + appNameText.text = appName } private fun loadProfile() { @@ -121,10 +148,17 @@ class HomeFragment : override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_main, menu) menu.removeItem(R.id.action_grid_size) menu.removeItem(R.id.action_layout_type) menu.removeItem(R.id.action_sort_order) menu.findItem(R.id.action_settings).setShowAsAction(SHOW_AS_ACTION_IF_ROOM) + ToolbarContentTintHelper.handleOnCreateOptionsMenu( + requireContext(), + toolbar, + menu, + ATHToolbarActivity.getToolbarBackgroundColor(toolbar) + ) } companion object { @@ -136,4 +170,28 @@ class HomeFragment : return HomeFragment() } } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_settings -> findNavController().navigate( + R.id.settingsActivity, + null, + navOptions + ) + R.id.action_import_playlist -> ImportPlaylistDialog().show( + childFragmentManager, + "ImportPlaylist" + ) + R.id.action_add_to_playlist -> CreatePlaylistDialog.create(emptyList()).show( + childFragmentManager, + "ShowCreatePlaylistDialog" + ) + } + return super.onOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar) + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt index 326e530c5..9744cb8e9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt @@ -11,7 +11,6 @@ import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.playlist.PlaylistAdapter import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewFragment -import com.google.android.material.transition.platform.MaterialFadeThrough import kotlinx.android.synthetic.main.fragment_library.* class PlaylistsFragment : AbsRecyclerViewFragment() { @@ -47,12 +46,12 @@ class PlaylistsFragment : AbsRecyclerViewFragment + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> diff --git a/app/src/main/res/layout/fragment_banner_home.xml b/app/src/main/res/layout/fragment_banner_home.xml index 11778e7e5..6db41162c 100644 --- a/app/src/main/res/layout/fragment_banner_home.xml +++ b/app/src/main/res/layout/fragment_banner_home.xml @@ -11,100 +11,142 @@ ~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ~ See the GNU General Public License for more details. --> - + android:layout_height="match_parent"> - + app:liftOnScroll="true"> - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + app:layout_scrollFlags="scroll|enterAlways"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_folder.xml b/app/src/main/res/layout/fragment_folder.xml index ffe3abd50..c366f24e9 100644 --- a/app/src/main/res/layout/fragment_folder.xml +++ b/app/src/main/res/layout/fragment_folder.xml @@ -1,63 +1,106 @@ - + android:layout_height="match_parent"> - + app:liftOnScroll="true"> - + app:layout_scrollFlags="scroll|enterAlways"> - + + + + + + + + + + + - + android:gravity="center" + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> - + - + + - + android:orientation="vertical"> - - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 1233a2a5e..2ccb28498 100755 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -11,66 +11,108 @@ ~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ~ See the GNU General Public License for more details. --> - + android:layout_height="match_parent"> - + android:layout_height="wrap_content" + app:liftOnScroll="true"> - - - + app:layout_scrollFlags="scroll|enterAlways"> - + - - + + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_recycler.xml b/app/src/main/res/layout/fragment_main_recycler.xml new file mode 100644 index 000000000..9342abdf6 --- /dev/null +++ b/app/src/main/res/layout/fragment_main_recycler.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index bd9c0e75d..ad6626bff 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/retro_graph" - app:startDestination="@id/libraryFragment"> + app:startDestination="@id/action_home"> + + + + + + + + + + + + + + + + + \ No newline at end of file From 209e3d58eb4573cfb35d82b61edf9ca0c3a5ee82 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 27 Sep 2020 02:09:07 +0530 Subject: [PATCH 27/72] Updated Item list to support big screen Fix search for playlist Fix playlist crash --- .../activities/LockScreenActivity.kt | 24 +-- .../retromusic/activities/MainActivity.kt | 13 +- .../activities/PlayingQueueActivity.kt | 20 +-- .../monkey/retromusic/adapter/GenreAdapter.kt | 3 - .../retromusic/adapter/SearchAdapter.kt | 40 +++-- .../adapter/base/MediaEntryViewHolder.java | 10 +- .../adapter/song/AbsOffsetSongAdapter.kt | 4 +- .../adapter/song/PlayingQueueAdapter.kt | 2 +- .../adapter/song/PlaylistSongAdapter.kt | 53 ++---- .../retromusic/adapter/song/SongAdapter.kt | 8 +- .../monkey/retromusic/db/SongExtension.kt | 7 + .../fragments/DetailListFragment.kt | 22 +-- .../retromusic/fragments/LibraryViewModel.kt | 34 ++-- .../fragments/albums/AlbumDetailsFragment.kt | 3 - .../artists/ArtistDetailsFragment.kt | 3 - .../fragments/base/AbsMainActivityFragment.kt | 3 + .../fragments/base/AbsPlayerFragment.kt | 3 - .../fragments/base/AbsRecyclerViewFragment.kt | 4 - .../fragments/folder/FoldersFragment.java | 2 + .../fragments/genres/GenreDetailsFragment.kt | 3 - .../retromusic/fragments/home/HomeFragment.kt | 5 +- .../fragments/library/LibraryFragment.kt | 4 - .../playlists/PlaylistDetailsFragment.kt | 96 +++-------- .../fragments/search/SearchFragment.kt | 3 - .../monkey/retromusic/model/PlaylistSong.kt | 2 +- .../repository/PlaylistSongsLoader.kt | 5 +- .../retromusic/repository/Repository.kt | 20 ++- .../retromusic/repository/SearchRepository.kt | 10 +- .../res/layout-land/fragment_banner_home.xml | 47 +++++- .../main/res/layout-land/fragment_home.xml | 153 ++++++++++------- .../layout-sw600dp/activity_playing_queue.xml | 155 ++++++++++++++++++ app/src/main/res/layout-sw600dp/item_list.xml | 123 ++++++++++++++ .../res/layout-sw600dp/item_list_no_image.xml | 64 ++++++++ .../main/res/layout-sw600dp/item_queue.xml | 126 ++++++++++++++ .../res/layout/activity_playing_queue.xml | 81 ++++++++- app/src/main/res/layout/item_list.xml | 136 +++++++-------- .../main/res/layout/item_list_no_image.xml | 5 +- app/src/main/res/layout/item_queue.xml | 92 +++++++---- app/src/main/res/layout/sub_header.xml | 3 +- .../main/res/master/values-af-rZA/strings.xml | 2 +- .../main/res/master/values-bn-rIN/strings.xml | 2 +- .../main/res/master/values-ca-rES/strings.xml | 2 +- .../main/res/master/values-da-rDK/strings.xml | 2 +- .../main/res/master/values-en-rUS/strings.xml | 2 +- .../main/res/master/values-fa-rIR/strings.xml | 2 +- .../main/res/master/values-fi-rFI/strings.xml | 2 +- .../main/res/master/values-hi-rIN/strings.xml | 2 +- .../main/res/master/values-iw-rIL/strings.xml | 2 +- .../main/res/master/values-kn-rIN/strings.xml | 2 +- .../main/res/master/values-ml-rIN/strings.xml | 2 +- .../main/res/master/values-ne-rIN/strings.xml | 2 +- .../main/res/master/values-no-rNO/strings.xml | 2 +- .../main/res/master/values-or-rIN/strings.xml | 2 +- .../main/res/master/values-pt-rPT/strings.xml | 2 +- .../main/res/master/values-sk-rSK/strings.xml | 2 +- .../main/res/master/values-sv-rSE/strings.xml | 2 +- .../main/res/master/values-ta-rIN/strings.xml | 2 +- .../main/res/master/values-ur-rIN/strings.xml | 2 +- app/src/main/res/values-af-rZA/strings.xml | 2 +- app/src/main/res/values-bn-rIN/strings.xml | 2 +- app/src/main/res/values-ca-rES/strings.xml | 2 +- app/src/main/res/values-da-rDK/strings.xml | 2 +- app/src/main/res/values-en-rHK/strings.xml | 2 +- app/src/main/res/values-en-rID/strings.xml | 2 +- app/src/main/res/values-en-rIN/strings.xml | 2 +- app/src/main/res/values-en-rUS/strings.xml | 2 +- app/src/main/res/values-fa-rIR/strings.xml | 2 +- app/src/main/res/values-fi-rFI/strings.xml | 2 +- app/src/main/res/values-hi-rIN/strings.xml | 2 +- app/src/main/res/values-iw-rIL/strings.xml | 2 +- app/src/main/res/values-kn-rIN/strings.xml | 2 +- app/src/main/res/values-ml-rIN/strings.xml | 2 +- app/src/main/res/values-ne-rIN/strings.xml | 2 +- app/src/main/res/values-no-rNO/strings.xml | 2 +- app/src/main/res/values-or-rIN/strings.xml | 2 +- app/src/main/res/values-pt-rPT/strings.xml | 2 +- app/src/main/res/values-sk-rSK/strings.xml | 2 +- app/src/main/res/values-ta-rIN/strings.xml | 2 +- app/src/main/res/values-ur-rIN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 80 files changed, 1025 insertions(+), 448 deletions(-) create mode 100644 app/src/main/res/layout-sw600dp/activity_playing_queue.xml create mode 100644 app/src/main/res/layout-sw600dp/item_list.xml create mode 100644 app/src/main/res/layout-sw600dp/item_list_no_image.xml create mode 100644 app/src/main/res/layout-sw600dp/item_queue.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt index 81ddfd8b6..9ff984373 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt @@ -28,16 +28,7 @@ class LockScreenActivity : AbsMusicServiceActivity() { override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() super.onCreate(savedInstanceState) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true) - val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - keyguardManager.requestDismissKeyguard(this, null) - } else { - this.window.addFlags( - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - ) - } + lockScreenInit() setContentView(R.layout.activity_lock_screen) hideStatusBar() setStatusbarColorAuto() @@ -77,6 +68,19 @@ class LockScreenActivity : AbsMusicServiceActivity() { } } + private fun lockScreenInit() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + val keyguardManager: KeyguardManager = getSystemService(KeyguardManager::class.java) + keyguardManager.requestDismissKeyguard(this, null) + } else { + this.window.addFlags( + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + ) + } + } + override fun onPlayingMetaChanged() { super.onPlayingMetaChanged() updateSongs() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index 2d4023d93..e11028fc1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -22,6 +22,7 @@ import code.name.monkey.retromusic.util.AppRater import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch +import org.koin.android.ext.android.get class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeListener { companion object { @@ -37,6 +38,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() super.onCreate(savedInstanceState) + setupNavigationController() if (!hasPermissions()) { findNavController(R.id.fragment_container).navigate(R.id.permissionFragment) } @@ -49,7 +51,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis updateTabs() //NavigationUI.setupWithNavController(getBottomNavigationView(), findNavController(R.id.fragment_container)) - setupNavigationController() + } private fun setupNavigationController() { @@ -121,8 +123,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis val id = parseLongFromIntent(intent, "playlistId", "playlist") if (id >= 0L) { val position: Int = intent.getIntExtra("position", 0) - val songs: List = - PlaylistSongsLoader.getPlaylistSongList(this@MainActivity, id) + val songs: List = PlaylistSongsLoader.getPlaylistSongList(get(), id) MusicPlayerRemote.openQueue(songs, position, true) handled = true } @@ -130,8 +131,9 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis val id = parseLongFromIntent(intent, "albumId", "album") if (id >= 0L) { val position: Int = intent.getIntExtra("position", 0) + val songs = libraryViewModel.albumById(id).songs MusicPlayerRemote.openQueue( - libraryViewModel.albumById(id).songs, + songs, position, true ) @@ -141,8 +143,9 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis val id = parseLongFromIntent(intent, "artistId", "artist") if (id >= 0L) { val position: Int = intent.getIntExtra("position", 0) + val songs: List = libraryViewModel.artistById(id).songs MusicPlayerRemote.openQueue( - libraryViewModel.artistById(id).songs, + songs, position, true ) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt index 9906db0b9..1218c71ce 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt @@ -13,7 +13,6 @@ import code.name.monkey.retromusic.adapter.song.PlayingQueueAdapter import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.surfaceColor import code.name.monkey.retromusic.helper.MusicPlayerRemote -import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.ThemedFastScroller import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager @@ -21,6 +20,7 @@ import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeMana import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import kotlinx.android.synthetic.main.activity_playing_queue.* +import kotlinx.android.synthetic.main.activity_playing_queue.title as NoImageTitle open class PlayingQueueActivity : AbsMusicServiceActivity() { @@ -31,13 +31,6 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { private var playingQueueAdapter: PlayingQueueAdapter? = null private lateinit var linearLayoutManager: LinearLayoutManager - private fun getUpNextAndQueueTime(): String { - val duration = MusicPlayerRemote.getQueueDurationMillis(MusicPlayerRemote.position) - return MusicUtil.buildInfoString( - resources.getString(R.string.up_next), - MusicUtil.getReadableDurationString(duration) - ) - } override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() @@ -54,7 +47,6 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { clearQueue.setOnClickListener { MusicPlayerRemote.clearQueue() } - checkForPadding() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -107,7 +99,8 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { ThemedFastScroller.create(recyclerView) } - private fun checkForPadding() { + override fun onServiceConnected() { + updateCurrentSong() } override fun onQueueChanged() { @@ -115,7 +108,6 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { finish() return } - checkForPadding() updateQueue() updateCurrentSong() } @@ -126,7 +118,9 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { } private fun updateCurrentSong() { - toolbar.subtitle = getUpNextAndQueueTime() + NoImageTitle.text = MusicPlayerRemote.currentSong.title + text.text = MusicPlayerRemote.currentSong.artistName + text2?.text = MusicPlayerRemote.currentSong.albumName } override fun onPlayingMetaChanged() { @@ -136,7 +130,6 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { private fun updateQueuePosition() { playingQueueAdapter?.setCurrent(MusicPlayerRemote.position) resetToCurrentPosition() - toolbar.subtitle = getUpNextAndQueueTime() } private fun updateQueue() { @@ -174,7 +167,6 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { } private fun setupToolbar() { - toolbar.subtitle = getUpNextAndQueueTime() toolbar.setBackgroundColor(surfaceColor()) setSupportActionBar(toolbar) clearQueue.backgroundTintList = ColorStateList.valueOf(accentColor()) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt index bf9f1c913..595eafaea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt @@ -1,6 +1,5 @@ package code.name.monkey.retromusic.adapter -import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -23,14 +22,12 @@ class GenreAdapter( var dataSet: List, private val mItemLayoutRes: Int ) : RecyclerView.Adapter() { - val colors = listOf(Color.RED, Color.BLUE) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(LayoutInflater.from(activity).inflate(mItemLayoutRes, parent, false)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val genre = dataSet[position] - holder.title?.text = genre.name holder.text?.text = String.format( Locale.getDefault(), diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt index 21d1b92a1..537f9b6ac 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt @@ -4,12 +4,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.navigation.findNavController import androidx.recyclerview.widget.RecyclerView import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.retromusic.* import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder +import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.ArtistGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote @@ -19,6 +22,7 @@ import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist import code.name.monkey.retromusic.repository.PlaylistSongsLoader import code.name.monkey.retromusic.util.MusicUtil import com.bumptech.glide.Glide +import java.util.* class SearchAdapter( private val activity: FragmentActivity, @@ -34,7 +38,7 @@ class SearchAdapter( if (dataSet[position] is Album) return ALBUM if (dataSet[position] is Artist) return ARTIST if (dataSet[position] is Genre) return GENRE - if (dataSet[position] is Playlist) return PLAYLIST + if (dataSet[position] is PlaylistWithSongs) return PLAYLIST return if (dataSet[position] is Song) SONG else HEADER } @@ -56,42 +60,52 @@ class SearchAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { when (getItemViewType(position)) { ALBUM -> { - val album = dataSet.get(position) as Album + holder. imageTextContainer?.isVisible = true + val album = dataSet[position] as Album holder.title?.text = album.title holder.text?.text = album.artistName AlbumGlideRequest.Builder.from(Glide.with(activity), album.safeGetFirstSong()) .checkIgnoreMediaStore().build().into(holder.image) } ARTIST -> { - val artist = dataSet.get(position) as Artist + holder. imageTextContainer?.isVisible = true + val artist = dataSet[position] as Artist holder.title?.text = artist.name holder.text?.text = MusicUtil.getArtistInfoString(activity, artist) ArtistGlideRequest.Builder.from(Glide.with(activity), artist).build() .into(holder.image) } SONG -> { - val song = dataSet.get(position) as Song + val song = dataSet[position] as Song holder.title?.text = song.title holder.text?.text = song.albumName } GENRE -> { - val genre = dataSet.get(position) as Genre + val genre = dataSet[position] as Genre holder.title?.text = genre.name + holder.text?.text = String.format( + Locale.getDefault(), + "%d %s", + genre.songCount, + if (genre.songCount > 1) activity.getString(R.string.songs) else activity.getString( + R.string.song + ) + ) } PLAYLIST -> { - val playlist = dataSet.get(position) as Playlist - holder.title?.text = playlist.name - holder.text?.text = MusicUtil.getPlaylistInfoString(activity, getSongs(playlist)) + val playlist = dataSet[position] as PlaylistWithSongs + holder.title?.text = playlist.playlistEntity.playlistName + holder.text?.text = MusicUtil.playlistInfoString(activity, playlist.songs) } else -> { - holder.title?.text = dataSet.get(position).toString() + holder.title?.text = dataSet[position].toString() holder.title?.setTextColor(ThemeStore.accentColor(activity)) } } } - private fun getSongs(playlist: Playlist): java.util.ArrayList { - val songs = java.util.ArrayList() + private fun getSongs(playlist: Playlist): List { + val songs = mutableListOf() if (playlist is AbsSmartPlaylist) { songs.addAll(playlist.getSongs()) } else { @@ -107,7 +121,7 @@ class SearchAdapter( inner class ViewHolder(itemView: View, itemViewType: Int) : MediaEntryViewHolder(itemView) { init { itemView.setOnLongClickListener(null) - + imageTextContainer?.isInvisible = true if (itemViewType == SONG) { menu?.visibility = View.VISIBLE menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) { @@ -156,7 +170,7 @@ class SearchAdapter( ) } SONG -> { - val playList = ArrayList() + val playList = mutableListOf() playList.add(item as Song) MusicPlayerRemote.openQueue(playList, 0, true) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java b/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java index 874601433..ae5587238 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java @@ -16,7 +16,6 @@ package code.name.monkey.retromusic.adapter.base; import android.graphics.Color; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -41,15 +40,13 @@ public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHold @Nullable public ImageView image; + @Nullable public ImageView artistImage; @Nullable public ImageView playerImage; - @Nullable - public ViewGroup imageContainer; - @Nullable public MaterialCardView imageContainerCard; @@ -77,6 +74,9 @@ public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHold @Nullable public TextView text; + @Nullable + public TextView text2; + @Nullable public TextView time; @@ -87,6 +87,7 @@ public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHold super(itemView); title = itemView.findViewById(R.id.title); text = itemView.findViewById(R.id.text); + text2 = itemView.findViewById(R.id.text2); image = itemView.findViewById(R.id.image); artistImage = itemView.findViewById(R.id.artistImage); @@ -94,7 +95,6 @@ public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHold time = itemView.findViewById(R.id.time); imageText = itemView.findViewById(R.id.imageText); - imageContainer = itemView.findViewById(R.id.imageContainer); imageTextContainer = itemView.findViewById(R.id.imageTextContainer); imageContainerCard = itemView.findViewById(R.id.imageContainerCard); diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt index 9904bc5b5..35cf5791d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt @@ -4,14 +4,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.LayoutRes -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song abstract class AbsOffsetSongAdapter( - activity: AppCompatActivity, + activity: FragmentActivity, dataSet: MutableList, @LayoutRes itemLayoutRes: Int, ICabHolder: ICabHolder? diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt index 2b1b9198a..7004909b8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt @@ -173,7 +173,7 @@ class PlayingQueueAdapter( } override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int { - return if (onCheckCanStartDrag(holder!!, position, x, y)) { + return if (onCheckCanStartDrag(holder, position, x, y)) { SwipeableItemConstants.REACTION_CAN_NOT_SWIPE_BOTH_H } else { SwipeableItemConstants.REACTION_CAN_SWIPE_BOTH_H diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt index 27f7edec6..0153f0e83 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt @@ -2,22 +2,21 @@ package code.name.monkey.retromusic.adapter.song import android.view.MenuItem import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.core.os.bundleOf -import androidx.navigation.findNavController -import code.name.monkey.retromusic.EXTRA_ALBUM_ID +import androidx.fragment.app.FragmentActivity import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.db.PlaylistEntity +import code.name.monkey.retromusic.db.toSongEntity +import code.name.monkey.retromusic.dialogs.RemoveSongFromPlaylistDialog import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song -import com.google.android.material.button.MaterialButton open class PlaylistSongAdapter( - activity: AppCompatActivity, + private val playlist: PlaylistEntity, + activity: FragmentActivity, dataSet: MutableList, itemLayoutRes: Int, ICabHolder: ICabHolder? -) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) { +) : SongAdapter(activity, dataSet, itemLayoutRes, ICabHolder) { init { this.setMultiSelectMenuRes(R.menu.menu_cannot_delete_single_songs_playlist_songs_selection) @@ -27,43 +26,21 @@ open class PlaylistSongAdapter( return ViewHolder(view) } - override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) { - if (holder.itemViewType == OFFSET_ITEM) { - val viewHolder = holder as ViewHolder - viewHolder.playAction?.let { - it.setOnClickListener { - MusicPlayerRemote.openQueue(dataSet, 0, true) - } - } - viewHolder.shuffleAction?.let { - it.setOnClickListener { - MusicPlayerRemote.openAndShuffleQueue(dataSet, true) - } - } - } else { - super.onBindViewHolder(holder, position - 1) - } - } - - open inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) { - - val playAction: MaterialButton? = itemView.findViewById(R.id.playAction) - val shuffleAction: MaterialButton? = itemView.findViewById(R.id.shuffleAction) + open inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) { override var songMenuRes: Int - get() = R.menu.menu_item_cannot_delete_single_songs_playlist_song + get() = R.menu.menu_item_playlist_song set(value) { super.songMenuRes = value } override fun onSongMenuItemClick(item: MenuItem): Boolean { - if (item.itemId == R.id.action_go_to_album) { - activity.findNavController(R.id.fragment_container) - .navigate( - R.id.albumDetailsFragment, - bundleOf(EXTRA_ALBUM_ID to song.albumId) - ) - return true + when (item.itemId) { + R.id.action_remove_from_playlist -> { + RemoveSongFromPlaylistDialog.create(song.toSongEntity(playlist.playListId)) + .show(activity.supportFragmentManager, "REMOVE_FROM_PLAYLIST") + return true + } } return super.onSongMenuItemClick(item) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index f7707c5a6..f5c18a47b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.navigation.findNavController import code.name.monkey.retromusic.EXTRA_ALBUM_ID @@ -87,6 +88,7 @@ open class SongAdapter( } holder.title?.text = getSongTitle(song) holder.text?.text = getSongText(song) + holder.text2?.text = getSongText(song) loadAlbumCover(song, holder) } @@ -121,6 +123,10 @@ open class SongAdapter( return song.artistName } + private fun getSongText2(song: Song): String? { + return song.albumName + } + override fun getItemCount(): Int { return dataSet.size } @@ -172,7 +178,7 @@ open class SongAdapter( } protected open fun onSongMenuItemClick(item: MenuItem): Boolean { - if (image != null && image!!.visibility == View.VISIBLE) { + if (image != null && image!!.isVisible) { when (item.itemId) { R.id.action_go_to_album -> { activity.findNavController(R.id.fragment_container) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt b/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt index 03666f0a2..ed1ea3bea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt @@ -2,6 +2,12 @@ package code.name.monkey.retromusic.db import code.name.monkey.retromusic.model.Song +fun List.fromHistoryToSongs(): List { + return map { + it.toSong() + } +} + fun List.toSongs(): List { return map { it.toSong() @@ -131,3 +137,4 @@ fun List.toSongsEntity(playlistEntity: PlaylistEntity): List { it.toSongEntity(playlistEntity.playListId) } } + diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index cd24eac7f..40d6bd217 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.core.os.bundleOf -import androidx.lifecycle.Observer import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -23,13 +22,12 @@ import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.state.NowPlayingPanelState +import code.name.monkey.retromusic.util.RetroUtil import kotlinx.android.synthetic.main.fragment_playlist_detail.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_detail), ArtistClickListener, AlbumClickListener { private val args by navArgs() - private val libraryViewModel by sharedViewModel() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) @@ -55,7 +53,6 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de TOP_PLAYED_PLAYLIST -> topPlayed() } - recyclerView.adapter?.registerAdapterDataObserver(object : AdapterDataObserver() { override fun onChanged() { super.onChanged() @@ -76,7 +73,7 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - libraryViewModel.recentSongs().observe(viewLifecycleOwner, Observer { songs -> + libraryViewModel.recentSongs().observe(viewLifecycleOwner, { songs -> songAdapter.swapDataSet(songs) }) } @@ -92,7 +89,7 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - libraryViewModel.playCountSongs().observe(viewLifecycleOwner, Observer { songs -> + libraryViewModel.playCountSongs().observe(viewLifecycleOwner, { songs -> songAdapter.swapDataSet(songs) }) } @@ -109,9 +106,8 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de adapter = songAdapter layoutManager = linearLayoutManager() } - libraryViewModel.observableHistorySongs().observe(viewLifecycleOwner, Observer { - val songs = it.map { historyEntity -> historyEntity.toSong() } - songAdapter.swapDataSet(songs) + libraryViewModel.observableHistorySongs().observe(viewLifecycleOwner, { + songAdapter.swapDataSet(it) }) } @@ -170,8 +166,14 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) private fun gridLayoutManager(): GridLayoutManager = - GridLayoutManager(requireContext(), 2, GridLayoutManager.VERTICAL, false) + GridLayoutManager(requireContext(), gridCount(), GridLayoutManager.VERTICAL, false) + private fun gridCount(): Int { + if (RetroUtil.isTablet()) { + return if (RetroUtil.isLandscape()) 6 else 4 + } + return 2 + } override fun onArtist(artistId: Long, imageView: ImageView) { findNavController().navigate( diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 2109bde0e..1fb477633 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -11,11 +11,8 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.repository.RealRepository - import code.name.monkey.retromusic.state.NowPlayingPanelState - import code.name.monkey.retromusic.util.PreferenceUtil - import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch @@ -39,6 +36,10 @@ class LibraryViewModel( panelState.postValue(state) } + init { + loadLibraryContent() + } + private fun loadLibraryContent() = viewModelScope.launch(IO) { fetchHomeSections() fetchSongs() @@ -51,37 +52,30 @@ class LibraryViewModel( fun getSearchResult(): LiveData> = searchResults fun getSongs(): LiveData> { - fetchSongs() return songs } fun getAlbums(): LiveData> { - fetchAlbums() return albums } fun getArtists(): LiveData> { - fetchArtists() return artists } fun getPlaylists(): LiveData> { - fetchPlaylists() return playlists } fun getLegacyPlaylist(): LiveData> { - fetchLegacyPlaylist() return legacyPlaylists } fun getGenre(): LiveData> { - fetchGenres() return genres } fun getHome(): LiveData> { - fetchHomeSections() return home } @@ -261,10 +255,6 @@ class LibraryViewModel( }) } - fun observableHistorySongs() = repository.observableHistorySongs() - - fun favorites() = repository.favorites() - fun artists(type: Int): LiveData> = liveData { when (type) { TOP_ARTISTS -> emit(repository.topArtists()) @@ -283,12 +273,6 @@ class LibraryViewModel( } } - fun clearSearchResult() { - viewModelScope.launch { - searchResults.postValue(emptyList()) - } - } - fun artist(artistId: Long): LiveData = liveData { emit(repository.artistById(artistId)) } @@ -296,6 +280,16 @@ class LibraryViewModel( fun fetchContributors(): LiveData> = liveData { emit(repository.contributor()) } + + fun observableHistorySongs() = repository.observableHistorySongs() + + fun favorites() = repository.favorites() + + fun clearSearchResult() { + viewModelScope.launch { + searchResults.postValue(emptyList()) + } + } } enum class ReloadType { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index 296d8955b..1544a6a3c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -30,7 +30,6 @@ import code.name.monkey.retromusic.dialogs.DeleteSongsDialog import code.name.monkey.retromusic.extensions.applyColor import code.name.monkey.retromusic.extensions.applyOutlineColor import code.name.monkey.retromusic.extensions.show -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.ArtistGlideRequest @@ -57,7 +56,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -69,7 +67,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det private val detailsViewModel by viewModel { parametersOf(arguments.extraAlbumId) } - private val libraryViewModel by sharedViewModel() private lateinit var simpleSongAdapter: SimpleSongAdapter private lateinit var album: Album diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt index 1caec7e6f..b3476552a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt @@ -26,7 +26,6 @@ import code.name.monkey.retromusic.extensions.applyColor import code.name.monkey.retromusic.extensions.applyOutlineColor import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.extensions.showToast -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.ArtistGlideRequest @@ -47,7 +46,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -59,7 +57,6 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d private val detailsViewModel: ArtistDetailsViewModel by viewModel { parametersOf(arguments.extraArtistId) } - private val libraryViewModel by sharedViewModel() private lateinit var artist: Artist private lateinit var songAdapter: SimpleSongAdapter private lateinit var albumAdapter: HorizontalAlbumAdapter diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMainActivityFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMainActivityFragment.kt index 22e373d5d..10b8094dd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMainActivityFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsMainActivityFragment.kt @@ -8,8 +8,11 @@ import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.VersionUtils import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.fragments.LibraryViewModel +import org.koin.androidx.viewmodel.ext.android.sharedViewModel abstract class AbsMainActivityFragment(@LayoutRes layout: Int) : AbsMusicServiceFragment(layout) { + val libraryViewModel: LibraryViewModel by sharedViewModel() val mainActivity: MainActivity get() = activity as MainActivity diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt index 8f067d478..473479d07 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerFragment.kt @@ -27,7 +27,6 @@ import code.name.monkey.retromusic.db.toSongEntity import code.name.monkey.retromusic.dialogs.* import code.name.monkey.retromusic.extensions.hide import code.name.monkey.retromusic.extensions.whichFragment -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote @@ -43,14 +42,12 @@ import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.io.FileNotFoundException abstract class AbsPlayerFragment(@LayoutRes layout: Int) : AbsMainActivityFragment(layout), Toolbar.OnMenuItemClickListener, IPaletteColorHolder, PlayerAlbumCoverFragment.Callbacks { private var playerAlbumCoverFragment: PlayerAlbumCoverFragment? = null - protected val libraryViewModel by sharedViewModel() override fun onMenuItemClick( item: MenuItem diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt index d7a82b159..c98ac32d8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt @@ -13,7 +13,6 @@ import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.R import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.DensityUtil @@ -23,15 +22,12 @@ import com.google.android.material.appbar.AppBarLayout import kotlinx.android.synthetic.main.fragment_main_recycler.* import me.zhanghai.android.fastscroll.FastScroller import me.zhanghai.android.fastscroll.FastScrollerBuilder -import org.koin.androidx.viewmodel.ext.android.sharedViewModel abstract class AbsRecyclerViewFragment, LM : RecyclerView.LayoutManager> : AbsMainActivityFragment(R.layout.fragment_main_recycler), AppBarLayout.OnOffsetChangedListener { - val libraryViewModel: LibraryViewModel by sharedViewModel() - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index ad0a4b7a0..43cd9ae58 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -75,6 +75,7 @@ import code.name.monkey.retromusic.misc.DialogAsyncTask; import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; import code.name.monkey.retromusic.misc.WrappedAsyncTaskLoader; import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.state.NowPlayingPanelState; import code.name.monkey.retromusic.util.DensityUtil; import code.name.monkey.retromusic.util.FileUtil; import code.name.monkey.retromusic.util.PreferenceUtil; @@ -163,6 +164,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); getMainActivity().setSupportActionBar(toolbar); getMainActivity().getSupportActionBar().setTitle(null); setStatusBarColorAuto(view); diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt index 8a5f7d522..cca506d1a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt @@ -12,14 +12,12 @@ import androidx.recyclerview.widget.RecyclerView import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.extensions.dipToPix -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.helper.menu.GenreMenuHelper import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.state.NowPlayingPanelState import kotlinx.android.synthetic.main.fragment_playlist_detail.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import java.util.* @@ -29,7 +27,6 @@ class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_ private val detailsViewModel: GenreDetailsViewModel by viewModel { parametersOf(arguments.extraGenre) } - private val libraryViewModel by sharedViewModel() private lateinit var genre: Genre private lateinit var songAdapter: SongAdapter diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index 905c30e4b..48c7f42c6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -37,24 +37,23 @@ import code.name.monkey.retromusic.adapter.HomeAdapter import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog import code.name.monkey.retromusic.extensions.findActivityNavController -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.ProfileBannerGlideRequest import code.name.monkey.retromusic.glide.UserProfileGlideRequest +import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.NavigationUtil import code.name.monkey.retromusic.util.PreferenceUtil import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.abs_playlists.* import kotlinx.android.synthetic.main.fragment_banner_home.* import kotlinx.android.synthetic.main.home_content.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel class HomeFragment : AbsMainActivityFragment(if (PreferenceUtil.isHomeBanner) R.layout.fragment_banner_home else R.layout.fragment_home) { - private val libraryViewModel: LibraryViewModel by sharedViewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null setStatusBarColorAuto(view) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 10a1630e2..3e5d579ba 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -15,18 +15,14 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog import code.name.monkey.retromusic.extensions.whichFragment -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.android.synthetic.main.fragment_library.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { - private val libraryViewModel by sharedViewModel() - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt index 641cab49d..6637984b4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt @@ -5,26 +5,20 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.view.isVisible import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.adapter.song.OrderablePlaylistSongAdapter -import code.name.monkey.retromusic.adapter.song.SongAdapter +import code.name.monkey.retromusic.adapter.song.PlaylistSongAdapter import code.name.monkey.retromusic.db.PlaylistWithSongs import code.name.monkey.retromusic.db.toSongs import code.name.monkey.retromusic.extensions.dipToPix -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.state.NowPlayingPanelState -import code.name.monkey.retromusic.util.PlaylistsUtil -import com.h6ah4i.android.widget.advrecyclerview.animator.RefactoredDefaultItemAnimator -import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager -import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import kotlinx.android.synthetic.main.fragment_playlist_detail.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -33,12 +27,9 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli private val viewModel: PlaylistDetailsViewModel by viewModel { parametersOf(arguments.extraPlaylist) } - private val libraryViewModel by sharedViewModel() - private lateinit var playlist: PlaylistWithSongs - private lateinit var adapter: SongAdapter - private var wrappedAdapter: RecyclerView.Adapter<*>? = null - private var recyclerViewDragDropManager: RecyclerViewDragDropManager? = null + private lateinit var playlist: PlaylistWithSongs + private lateinit var playlistSongAdapter: PlaylistSongAdapter override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) @@ -58,39 +49,19 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli } private fun setUpRecyclerView() { - recyclerView.layoutManager = LinearLayoutManager(requireContext()) - recyclerViewDragDropManager = RecyclerViewDragDropManager() - val animator = RefactoredDefaultItemAnimator() - adapter = - OrderablePlaylistSongAdapter( - playlist.playlistEntity, - requireActivity(), - ArrayList(), - R.layout.item_list, - null, - object : OrderablePlaylistSongAdapter.OnMoveItemListener { - override fun onMoveItem(fromPosition: Int, toPosition: Int) { - if (PlaylistsUtil.moveItem( - requireContext(), - playlist.playlistEntity.playListId, - fromPosition, - toPosition - ) - ) { - val song = adapter.dataSet.removeAt(fromPosition) - adapter.dataSet.add(toPosition, song) - adapter.notifyItemMoved(fromPosition, toPosition) - } - } - }) - wrappedAdapter = recyclerViewDragDropManager!!.createWrappedAdapter(adapter) - - recyclerView.adapter = wrappedAdapter - recyclerView.itemAnimator = animator - - recyclerViewDragDropManager?.attachRecyclerView(recyclerView) - - adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + playlistSongAdapter = PlaylistSongAdapter( + playlist.playlistEntity, + requireActivity(), + ArrayList(), + R.layout.item_list, + null, + ) + recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = playlistSongAdapter + } + playlistSongAdapter.registerAdapterDataObserver(object : + RecyclerView.AdapterDataObserver() { override fun onChanged() { super.onChanged() checkIsEmpty() @@ -100,10 +71,7 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - val menuRes =/* if (playlist is AbsCustomPlaylist) - R.menu.menu_smart_playlist_detail - else*/ R.menu.menu_playlist_detail - inflater.inflate(menuRes, menu) + inflater.inflate(R.menu.menu_playlist_detail, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -117,32 +85,14 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli private fun checkIsEmpty() { checkForPadding() - empty.visibility = if (adapter.itemCount == 0) View.VISIBLE else View.GONE - emptyText.visibility = if (adapter.itemCount == 0) View.VISIBLE else View.GONE + empty.isVisible = playlistSongAdapter.itemCount == 0 + emptyText.isVisible = playlistSongAdapter.itemCount == 0 } - override fun onPause() { - if (recyclerViewDragDropManager != null) { - recyclerViewDragDropManager!!.cancelDrag() - } - super.onPause() - } override fun onDestroy() { - if (recyclerViewDragDropManager != null) { - recyclerViewDragDropManager!!.release() - recyclerViewDragDropManager = null - } - - if (recyclerView != null) { - recyclerView!!.itemAnimator = null - recyclerView!!.adapter = null - } - - if (wrappedAdapter != null) { - WrapperAdapterUtils.releaseAll(wrappedAdapter) - wrappedAdapter = null - } + recyclerView?.itemAnimator = null + recyclerView?.adapter = null super.onDestroy() } @@ -154,7 +104,7 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli fun songs(songs: List) { progressIndicator.hide() if (songs.isNotEmpty()) { - adapter.swapDataSet(songs) + playlistSongAdapter.swapDataSet(songs) } else { showEmptyView() } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt index f2dd37a34..3f5f9f062 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt @@ -20,12 +20,10 @@ import code.name.monkey.retromusic.adapter.SearchAdapter import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.dipToPix import code.name.monkey.retromusic.extensions.showToast -import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.state.NowPlayingPanelState import com.google.android.material.textfield.TextInputEditText import kotlinx.android.synthetic.main.fragment_search.* -import org.koin.androidx.viewmodel.ext.android.sharedViewModel import java.util.* import kotlin.collections.ArrayList @@ -35,7 +33,6 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa const val REQ_CODE_SPEECH_INPUT = 9001 } - private val libraryViewModel by sharedViewModel() private lateinit var searchAdapter: SearchAdapter private var query: String? = null diff --git a/app/src/main/java/code/name/monkey/retromusic/model/PlaylistSong.kt b/app/src/main/java/code/name/monkey/retromusic/model/PlaylistSong.kt index 8b7cbc00b..d972e3181 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/PlaylistSong.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/PlaylistSong.kt @@ -33,7 +33,7 @@ class PlaylistSong( override val artistName: String, val playlistId: Long, val idInPlayList: Long, - override val composer: String, + override val composer: String?, override val albumArtist: String? ) : Song( id = id, diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/PlaylistSongsLoader.kt b/app/src/main/java/code/name/monkey/retromusic/repository/PlaylistSongsLoader.kt index fa24d931e..80f0829a8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/PlaylistSongsLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/PlaylistSongsLoader.kt @@ -16,9 +16,8 @@ package code.name.monkey.retromusic.repository import android.content.Context import android.database.Cursor -import android.provider.MediaStore import android.provider.MediaStore.Audio.AudioColumns -import android.provider.MediaStore.Audio.Playlists.* +import android.provider.MediaStore.Audio.Playlists.Members import code.name.monkey.retromusic.Constants.IS_MUSIC import code.name.monkey.retromusic.extensions.getInt import code.name.monkey.retromusic.extensions.getLong @@ -83,7 +82,7 @@ object PlaylistSongsLoader { val artistId = cursor.getLong(AudioColumns.ARTIST_ID) val artistName = cursor.getString(AudioColumns.ARTIST) val idInPlaylist = cursor.getLong(Members._ID) - val composer = cursor.getString(AudioColumns.COMPOSER) + val composer = cursor.getStringOrNull(AudioColumns.COMPOSER) val albumArtist = cursor.getStringOrNull("album_artist") return PlaylistSong( id, diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt index 2bf9c67ff..476dac747 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt @@ -16,6 +16,7 @@ package code.name.monkey.retromusic.repository import android.content.Context import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import code.name.monkey.retromusic.* import code.name.monkey.retromusic.db.* import code.name.monkey.retromusic.model.* @@ -39,7 +40,7 @@ interface Repository { fun genresFlow(): Flow>> fun historySong(): List fun favorites(): LiveData> - fun observableHistorySongs(): LiveData> + fun observableHistorySongs(): LiveData> fun albumById(albumId: Long): Album fun playlistSongs(playlistEntity: PlaylistEntity): LiveData> suspend fun fetchAlbums(): List @@ -97,6 +98,9 @@ interface Repository { suspend fun blackListPaths(): List suspend fun deleteSongs(songs: List) suspend fun contributor(): List + suspend fun searchArtists(query: String): List + suspend fun searchSongs(query: String): List + suspend fun searchAlbums(query: String): List } class RealRepository( @@ -115,11 +119,17 @@ class RealRepository( ) : Repository { - override suspend fun deleteSongs(songs: List) = roomRepository.deleteSongs(songs) override suspend fun contributor(): List = localDataRepository.contributors() + override suspend fun searchSongs(query: String): List = songRepository.songs(query) + + override suspend fun searchAlbums(query: String): List = albumRepository.albums(query) + + override suspend fun searchArtists(query: String): List = + artistRepository.artists(query) + override suspend fun fetchAlbums(): List = albumRepository.albums() override suspend fun albumByIdAsync(albumId: Long): Album = albumRepository.album(albumId) @@ -311,8 +321,10 @@ class RealRepository( override suspend fun blackListPaths(): List = roomRepository.blackListPaths() - override fun observableHistorySongs(): LiveData> = - roomRepository.observableHistorySongs() + override fun observableHistorySongs(): LiveData> = + Transformations.map(roomRepository.observableHistorySongs()) { + it.fromHistoryToSongs() + } override fun historySong(): List = roomRepository.historySongs() diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SearchRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/SearchRepository.kt index 3aa55242a..555ead5eb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SearchRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SearchRepository.kt @@ -22,11 +22,11 @@ import java.util.* class RealSearchRepository( private val songRepository: SongRepository, private val albumRepository: AlbumRepository, - private val artistRepository: RealArtistRepository, + private val artistRepository: ArtistRepository, + private val roomRepository: RoomRepository, private val genreRepository: GenreRepository, - private val playlistRepository: PlaylistRepository ) { - fun searchAll(context: Context, query: String?): MutableList { + suspend fun searchAll(context: Context, query: String?): MutableList { val results = mutableListOf() query?.let { searchString -> val songs = songRepository.songs(searchString) @@ -53,8 +53,8 @@ class RealSearchRepository( results.add(context.resources.getString(R.string.genres)) results.addAll(genres) } - val playlist = playlistRepository.playlists().filter { playlist -> - playlist.name.toLowerCase(Locale.getDefault()) + val playlist = roomRepository.playlistWithSongs().filter { playlist -> + playlist.playlistEntity.playlistName.toLowerCase(Locale.getDefault()) .contains(searchString.toLowerCase(Locale.getDefault())) } if (playlist.isNotEmpty()) { diff --git a/app/src/main/res/layout-land/fragment_banner_home.xml b/app/src/main/res/layout-land/fragment_banner_home.xml index 83422bb38..0b13fab3a 100644 --- a/app/src/main/res/layout-land/fragment_banner_home.xml +++ b/app/src/main/res/layout-land/fragment_banner_home.xml @@ -11,10 +11,50 @@ ~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ~ See the GNU General Public License for more details. --> - - + + + + + + + + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_home.xml b/app/src/main/res/layout-land/fragment_home.xml index a2e570c37..a26bf9e55 100644 --- a/app/src/main/res/layout-land/fragment_home.xml +++ b/app/src/main/res/layout-land/fragment_home.xml @@ -11,68 +11,109 @@ ~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ~ See the GNU General Public License for more details. --> - - + android:layout_height="match_parent"> - + android:layout_height="wrap_content" + app:liftOnScroll="true"> - - - + app:layout_scrollFlags="scroll|enterAlways"> - + - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_playing_queue.xml b/app/src/main/res/layout-sw600dp/activity_playing_queue.xml new file mode 100644 index 000000000..a0da4e0fb --- /dev/null +++ b/app/src/main/res/layout-sw600dp/activity_playing_queue.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_list.xml b/app/src/main/res/layout-sw600dp/item_list.xml new file mode 100644 index 000000000..7a5f59b12 --- /dev/null +++ b/app/src/main/res/layout-sw600dp/item_list.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_list_no_image.xml b/app/src/main/res/layout-sw600dp/item_list_no_image.xml new file mode 100644 index 000000000..dfcbafa8b --- /dev/null +++ b/app/src/main/res/layout-sw600dp/item_list_no_image.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/item_queue.xml b/app/src/main/res/layout-sw600dp/item_queue.xml new file mode 100644 index 000000000..20c4531ec --- /dev/null +++ b/app/src/main/res/layout-sw600dp/item_queue.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_playing_queue.xml b/app/src/main/res/layout/activity_playing_queue.xml index cbc1378d6..4dcdf70f3 100755 --- a/app/src/main/res/layout/activity_playing_queue.xml +++ b/app/src/main/res/layout/activity_playing_queue.xml @@ -37,15 +37,79 @@ - + android:layout_height="wrap_content" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/item_list.xml b/app/src/main/res/layout/item_list.xml index fac82e1ea..aefd963be 100755 --- a/app/src/main/res/layout/item_list.xml +++ b/app/src/main/res/layout/item_list.xml @@ -1,5 +1,5 @@ - + tools:ignore="ContentDescription" + tools:visibility="visible" /> - - - - - - - - - - - + android:layout_marginStart="16dp" + app:cardCornerRadius="6dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/drag_view" + app:layout_constraintTop_toTopOf="parent"> + + + android:minHeight="40dp" + android:textAppearance="@style/TextViewSubtitle2" + android:visibility="gone" + tools:text="100" + tools:visibility="visible" /> + - - + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_list_no_image.xml b/app/src/main/res/layout/item_list_no_image.xml index bf6ffc263..6331125d8 100644 --- a/app/src/main/res/layout/item_list_no_image.xml +++ b/app/src/main/res/layout/item_list_no_image.xml @@ -32,7 +32,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:text="@tools:sample/first_names" /> + tools:text="@tools:sample/full_names" /> - + tools:text="@tools:sample/full_names" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_queue.xml b/app/src/main/res/layout/item_queue.xml index 1ac4b428e..33082b0c7 100644 --- a/app/src/main/res/layout/item_queue.xml +++ b/app/src/main/res/layout/item_queue.xml @@ -13,11 +13,16 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -26,71 +31,86 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center_vertical|start" - android:padding="8dp" android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_drag_vertical" app:tint="?attr/colorControlNormal" tools:ignore="ContentDescription" tools:visibility="visible" /> + + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + app:cardCornerRadius="6dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/drag_view" + app:layout_constraintTop_toTopOf="parent"> + - - + android:ellipsize="end" + android:maxLines="1" + android:paddingHorizontal="16dp" + android:textAppearance="@style/TextViewSubtitle1" + android:textColor="?android:attr/textColorPrimary" + app:layout_constraintBottom_toTopOf="@+id/text" + app:layout_constraintEnd_toStartOf="@id/menu" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@id/imageContainer" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="@tools:sample/full_names" /> - - - - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/sub_header.xml b/app/src/main/res/layout/sub_header.xml index 5a638f255..5b066f07f 100644 --- a/app/src/main/res/layout/sub_header.xml +++ b/app/src/main/res/layout/sub_header.xml @@ -6,7 +6,8 @@ android:layout_height="wrap_content" android:layout_gravity="start" android:gravity="start" - android:paddingHorizontal="16dp" + android:layout_marginStart="@dimen/toolbar_margin_horizontal" + android:layout_marginEnd="@dimen/toolbar_margin_horizontal" android:paddingVertical="12dp" android:textAppearance="@style/TextViewOverline" android:textStyle="bold" diff --git a/app/src/main/res/master/values-af-rZA/strings.xml b/app/src/main/res/master/values-af-rZA/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-af-rZA/strings.xml +++ b/app/src/main/res/master/values-af-rZA/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-bn-rIN/strings.xml b/app/src/main/res/master/values-bn-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-bn-rIN/strings.xml +++ b/app/src/main/res/master/values-bn-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-ca-rES/strings.xml b/app/src/main/res/master/values-ca-rES/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-ca-rES/strings.xml +++ b/app/src/main/res/master/values-ca-rES/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-da-rDK/strings.xml b/app/src/main/res/master/values-da-rDK/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-da-rDK/strings.xml +++ b/app/src/main/res/master/values-da-rDK/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-en-rUS/strings.xml b/app/src/main/res/master/values-en-rUS/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-en-rUS/strings.xml +++ b/app/src/main/res/master/values-en-rUS/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-fa-rIR/strings.xml b/app/src/main/res/master/values-fa-rIR/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-fa-rIR/strings.xml +++ b/app/src/main/res/master/values-fa-rIR/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-fi-rFI/strings.xml b/app/src/main/res/master/values-fi-rFI/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-fi-rFI/strings.xml +++ b/app/src/main/res/master/values-fi-rFI/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-hi-rIN/strings.xml b/app/src/main/res/master/values-hi-rIN/strings.xml index 56dbeb11c..d9822f7ae 100644 --- a/app/src/main/res/master/values-hi-rIN/strings.xml +++ b/app/src/main/res/master/values-hi-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-iw-rIL/strings.xml b/app/src/main/res/master/values-iw-rIL/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-iw-rIL/strings.xml +++ b/app/src/main/res/master/values-iw-rIL/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-kn-rIN/strings.xml b/app/src/main/res/master/values-kn-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-kn-rIN/strings.xml +++ b/app/src/main/res/master/values-kn-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-ml-rIN/strings.xml b/app/src/main/res/master/values-ml-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-ml-rIN/strings.xml +++ b/app/src/main/res/master/values-ml-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-ne-rIN/strings.xml b/app/src/main/res/master/values-ne-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-ne-rIN/strings.xml +++ b/app/src/main/res/master/values-ne-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-no-rNO/strings.xml b/app/src/main/res/master/values-no-rNO/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-no-rNO/strings.xml +++ b/app/src/main/res/master/values-no-rNO/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-or-rIN/strings.xml b/app/src/main/res/master/values-or-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-or-rIN/strings.xml +++ b/app/src/main/res/master/values-or-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-pt-rPT/strings.xml b/app/src/main/res/master/values-pt-rPT/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-pt-rPT/strings.xml +++ b/app/src/main/res/master/values-pt-rPT/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-sk-rSK/strings.xml b/app/src/main/res/master/values-sk-rSK/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-sk-rSK/strings.xml +++ b/app/src/main/res/master/values-sk-rSK/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-sv-rSE/strings.xml b/app/src/main/res/master/values-sv-rSE/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-sv-rSE/strings.xml +++ b/app/src/main/res/master/values-sv-rSE/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-ta-rIN/strings.xml b/app/src/main/res/master/values-ta-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-ta-rIN/strings.xml +++ b/app/src/main/res/master/values-ta-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/master/values-ur-rIN/strings.xml b/app/src/main/res/master/values-ur-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/master/values-ur-rIN/strings.xml +++ b/app/src/main/res/master/values-ur-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-en-rHK/strings.xml b/app/src/main/res/values-en-rHK/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/values-en-rHK/strings.xml +++ b/app/src/main/res/values-en-rHK/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-en-rID/strings.xml b/app/src/main/res/values-en-rID/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/values-en-rID/strings.xml +++ b/app/src/main/res/values-en-rID/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-en-rIN/strings.xml b/app/src/main/res/values-en-rIN/strings.xml index 1bb735e39..e028f27cb 100644 --- a/app/src/main/res/values-en-rIN/strings.xml +++ b/app/src/main/res/values-en-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-en-rUS/strings.xml +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index c1622e83e..d55510221 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index c432a0a58..fa0ac4a38 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-ne-rIN/strings.xml b/app/src/main/res/values-ne-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-ne-rIN/strings.xml +++ b/app/src/main/res/values-ne-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-no-rNO/strings.xml b/app/src/main/res/values-no-rNO/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-no-rNO/strings.xml +++ b/app/src/main/res/values-no-rNO/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index ed335f517..e14f5f04e 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values-ur-rIN/strings.xml b/app/src/main/res/values-ur-rIN/strings.xml index d2b1c3ba9..c44382282 100644 --- a/app/src/main/res/values-ur-rIN/strings.xml +++ b/app/src/main/res/values-ur-rIN/strings.xml @@ -370,7 +370,7 @@ Profile Purchase *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app Love this app? Let us know in the Google Play Store how we can make it even better Recent albums diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c72762535..7bb2c04d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -604,7 +604,7 @@ *Think before buying, don\'t ask for refund. - Queue + Playing Queue Rate the app From b78cbb60b4737f9dceac82bdd4c8189c5e366cfa Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 27 Sep 2020 03:03:56 +0530 Subject: [PATCH 28/72] Update SearchAdapter.kt --- .../java/code/name/monkey/retromusic/adapter/SearchAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt index 537f9b6ac..6fe434c2c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt @@ -165,8 +165,8 @@ class SearchAdapter( } PLAYLIST -> { activity.findNavController(R.id.fragment_container).navigate( - R.id.artistDetailsFragment, - bundleOf(EXTRA_PLAYLIST to (item as Playlist)) + R.id.playlistDetailsFragment, + bundleOf(EXTRA_PLAYLIST to (item as PlaylistWithSongs)) ) } SONG -> { From fdfedf274f262b150b3b067082eb3911b3e5a1d3 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 28 Sep 2020 00:29:09 +0530 Subject: [PATCH 29/72] Added some animations Renamed some interface classes --- .../retromusic/activities/LyricsActivity.kt | 23 ++++ .../retromusic/activities/MainActivity.kt | 10 +- .../activities/PlayingQueueActivity.kt | 22 ++-- .../monkey/retromusic/adapter/HomeAdapter.kt | 13 +-- .../retromusic/adapter/album/AlbumAdapter.kt | 12 ++- .../adapter/album/AlbumCoverPagerAdapter.kt | 12 ++- .../adapter/album/HorizontalAlbumAdapter.kt | 4 +- .../adapter/artist/ArtistAdapter.kt | 15 ++- .../fragments/DetailListFragment.kt | 15 +-- .../fragments/albums/AlbumDetailsFragment.kt | 25 +++-- .../fragments/albums/AlbumsFragment.kt | 13 +-- .../artists/ArtistDetailsFragment.kt | 24 ++++- .../fragments/artists/ArtistsFragment.kt | 21 ++-- .../fragments/base/AbsRecyclerViewFragment.kt | 9 ++ .../retromusic/fragments/home/HomeFragment.kt | 7 +- .../player/full/FullPlayerFragment.kt | 9 +- .../interfaces/IAlbumClickListener.kt | 7 ++ .../interfaces/IArtistClickListener.kt | 7 ++ .../layout-sw600dp/activity_playing_queue.xml | 100 ++---------------- .../res/layout/activity_playing_queue.xml | 82 ++------------ .../res/layout/fragment_album_details.xml | 7 +- 21 files changed, 190 insertions(+), 247 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt create mode 100644 app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt index 2c64c8a31..a40868424 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt @@ -3,7 +3,9 @@ package code.name.monkey.retromusic.activities import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.View import android.view.WindowManager +import androidx.interpolator.view.animation.FastOutSlowInInterpolator import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.R @@ -15,8 +17,13 @@ import code.name.monkey.retromusic.lyrics.LrcView import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.LyricUtil import code.name.monkey.retromusic.util.RetroUtil +import com.google.android.material.color.MaterialColors +import com.google.android.material.transition.platform.MaterialArcMotion +import com.google.android.material.transition.platform.MaterialContainerTransform +import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import kotlinx.android.synthetic.main.activity_lyrics.* + class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper.Callback { private lateinit var updateHelper: MusicProgressViewUpdateHelper @@ -31,7 +38,23 @@ class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper. return baseUrl } + private fun buildContainerTransform( ): MaterialContainerTransform { + val transform = MaterialContainerTransform() + transform.setAllContainerColors( + MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface) + ) + transform.addTarget(android.R.id.content) + transform.duration = 300 + transform.interpolator = FastOutSlowInInterpolator() + transform.pathMotion = MaterialArcMotion() + return transform + } + override fun onCreate(savedInstanceState: Bundle?) { + findViewById(android.R.id.content).transitionName = "lyrics" + setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) + window.sharedElementEnterTransition = buildContainerTransform( ) + window.sharedElementReturnTransition = buildContainerTransform( ) super.onCreate(savedInstanceState) setContentView(R.layout.activity_lyrics) setStatusbarColorAuto() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index e11028fc1..8c91bf4ef 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -38,10 +38,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() super.onCreate(savedInstanceState) - setupNavigationController() - if (!hasPermissions()) { - findNavController(R.id.fragment_container).navigate(R.id.permissionFragment) - } + setStatusbarColorAuto() setNavigationbarColorAuto() setLightNavigationBar(true) @@ -51,7 +48,10 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis updateTabs() //NavigationUI.setupWithNavController(getBottomNavigationView(), findNavController(R.id.fragment_container)) - + setupNavigationController() + if (!hasPermissions()) { + findNavController(R.id.fragment_container).navigate(R.id.permissionFragment) + } } private fun setupNavigationController() { diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt index 1218c71ce..b02622f86 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt @@ -13,6 +13,7 @@ import code.name.monkey.retromusic.adapter.song.PlayingQueueAdapter import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.surfaceColor import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.ThemedFastScroller import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager @@ -20,7 +21,6 @@ import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeMana import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils import kotlinx.android.synthetic.main.activity_playing_queue.* -import kotlinx.android.synthetic.main.activity_playing_queue.title as NoImageTitle open class PlayingQueueActivity : AbsMusicServiceActivity() { @@ -31,6 +31,13 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { private var playingQueueAdapter: PlayingQueueAdapter? = null private lateinit var linearLayoutManager: LinearLayoutManager + private fun getUpNextAndQueueTime(): String { + val duration = MusicPlayerRemote.getQueueDurationMillis(MusicPlayerRemote.position) + return MusicUtil.buildInfoString( + resources.getString(R.string.up_next), + MusicUtil.getReadableDurationString(duration) + ) + } override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() @@ -47,6 +54,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { clearQueue.setOnClickListener { MusicPlayerRemote.clearQueue() } + checkForPadding() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -99,8 +107,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { ThemedFastScroller.create(recyclerView) } - override fun onServiceConnected() { - updateCurrentSong() + private fun checkForPadding() { } override fun onQueueChanged() { @@ -108,6 +115,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { finish() return } + checkForPadding() updateQueue() updateCurrentSong() } @@ -118,9 +126,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { } private fun updateCurrentSong() { - NoImageTitle.text = MusicPlayerRemote.currentSong.title - text.text = MusicPlayerRemote.currentSong.artistName - text2?.text = MusicPlayerRemote.currentSong.albumName + toolbar.subtitle = getUpNextAndQueueTime() } override fun onPlayingMetaChanged() { @@ -130,6 +136,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { private fun updateQueuePosition() { playingQueueAdapter?.setCurrent(MusicPlayerRemote.position) resetToCurrentPosition() + toolbar.subtitle = getUpNextAndQueueTime() } private fun updateQueue() { @@ -167,6 +174,7 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { } private fun setupToolbar() { + toolbar.subtitle = getUpNextAndQueueTime() toolbar.setBackgroundColor(surfaceColor()) setSupportActionBar(toolbar) clearQueue.backgroundTintList = ColorStateList.valueOf(accentColor()) @@ -177,4 +185,4 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { clearQueue.iconTint = this } } -} +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt index 4c161daf7..86e272e4e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt @@ -20,10 +20,10 @@ import code.name.monkey.retromusic.adapter.album.AlbumAdapter import code.name.monkey.retromusic.adapter.artist.ArtistAdapter import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.fragments.albums.AlbumClickListener -import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.interfaces.IAlbumClickListener +import code.name.monkey.retromusic.interfaces.IArtistClickListener import code.name.monkey.retromusic.model.* import code.name.monkey.retromusic.util.PreferenceUtil import com.bumptech.glide.Glide @@ -31,7 +31,7 @@ import com.google.android.material.card.MaterialCardView class HomeAdapter( private val activity: AppCompatActivity -) : RecyclerView.Adapter(), ArtistClickListener, AlbumClickListener { +) : RecyclerView.Adapter(), IArtistClickListener, IAlbumClickListener { private var list = listOf() @@ -231,15 +231,16 @@ class HomeAdapter( AlbumAdapter(activity, albums, PreferenceUtil.homeAlbumGridStyle, null, this) fun gridLayoutManager() = GridLayoutManager(activity, 1, GridLayoutManager.HORIZONTAL, false) + fun linearLayoutManager() = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false) - override fun onArtist(artistId: Long, imageView: ImageView) { + override fun onArtist(artistId: Long, view: View) { activity.findNavController(R.id.fragment_container).navigate( R.id.artistDetailsFragment, bundleOf(EXTRA_ARTIST_ID to artistId), null, FragmentNavigatorExtras( - imageView to activity.getString(R.string.transition_album_art) + view to "artist" ) ) } @@ -250,7 +251,7 @@ class HomeAdapter( bundleOf(EXTRA_ALBUM_ID to albumId), null, FragmentNavigatorExtras( - view to activity.getString(R.string.transition_album_art) + view to "album" ) ) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt index 2d3896cbc..2461fad5e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt @@ -6,16 +6,17 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.core.view.ViewCompat import androidx.fragment.app.FragmentActivity import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder -import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.menu.SongsMenuHelper +import code.name.monkey.retromusic.interfaces.IAlbumClickListener import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Song @@ -30,7 +31,7 @@ open class AlbumAdapter( var dataSet: List, protected var itemLayoutRes: Int, ICabHolder: ICabHolder?, - private val albumClickListener: AlbumClickListener? + private val albumClickListener: IAlbumClickListener? ) : AbsMultiSelectAdapter( activity, ICabHolder, @@ -75,7 +76,7 @@ open class AlbumAdapter( holder.title?.text = getAlbumTitle(album) holder.text?.text = getAlbumText(album) holder.playSongs?.setOnClickListener { - album.songs?.let { songs -> + album.songs.let { songs -> MusicPlayerRemote.openQueue( songs, 0, @@ -116,7 +117,7 @@ open class AlbumAdapter( } override fun getItemId(position: Int): Long { - return dataSet[position].id.toLong() + return dataSet[position].id } override fun getIdentifier(position: Int): Album? { @@ -161,7 +162,7 @@ open class AlbumAdapter( inner class ViewHolder(itemView: View) : MediaEntryViewHolder(itemView) { init { - setImageTransitionName(activity.getString(R.string.transition_album_art)) + setImageTransitionName("Album") menu?.visibility = View.GONE } @@ -170,6 +171,7 @@ open class AlbumAdapter( if (isInQuickSelectMode) { toggleChecked(layoutPosition) } else { + ViewCompat.setTransitionName(itemView, "album") albumClickListener?.onAlbumClick(dataSet[layoutPosition].id, itemView) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index 11aa9560d..d48fe2d44 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -1,14 +1,17 @@ package code.name.monkey.retromusic.adapter.album +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import androidx.core.app.ActivityOptionsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.LyricsActivity import code.name.monkey.retromusic.fragments.AlbumCoverStyle import code.name.monkey.retromusic.fragments.NowPlayingScreen.* import code.name.monkey.retromusic.glide.RetroMusicColoredTarget @@ -90,8 +93,13 @@ class AlbumCoverPagerAdapter( val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false) albumCover = view.findViewById(R.id.player_image) albumCover.setOnClickListener { - //LyricsDialog().show(childFragmentManager, "LyricsDialog") - showLyricsDialog() + val intent = Intent(requireContext(), LyricsActivity::class.java) + val activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + it, + "lyrics" + ) + startActivity(intent, activityOptions.toBundle()) } return view } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt index 7ff459ec3..6f743b6b4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt @@ -3,10 +3,10 @@ package code.name.monkey.retromusic.adapter.album import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentActivity -import code.name.monkey.retromusic.fragments.albums.AlbumClickListener import code.name.monkey.retromusic.glide.AlbumGlideRequest import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.HorizontalAdapterHelper +import code.name.monkey.retromusic.interfaces.IAlbumClickListener import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.util.MusicUtil @@ -17,7 +17,7 @@ class HorizontalAlbumAdapter( activity: FragmentActivity, dataSet: List, ICabHolder: ICabHolder?, - albumClickListener: AlbumClickListener + albumClickListener: IAlbumClickListener ) : AlbumAdapter( activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, ICabHolder, albumClickListener ) { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt index 7f32e9848..e925576cc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt @@ -12,10 +12,10 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.glide.ArtistGlideRequest import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.helper.menu.SongsMenuHelper +import code.name.monkey.retromusic.interfaces.IArtistClickListener import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Song @@ -29,8 +29,8 @@ class ArtistAdapter( val activity: FragmentActivity, var dataSet: List, var itemLayoutRes: Int, - ICabHolder: ICabHolder?, - private val artistClickListener: ArtistClickListener + val ICabHolder: ICabHolder?, + val IArtistClickListener: IArtistClickListener ) : AbsMultiSelectAdapter( activity, ICabHolder, R.menu.menu_media_selection ), PopupTextProvider { @@ -45,7 +45,7 @@ class ArtistAdapter( } override fun getItemId(position: Int): Long { - return dataSet[position].id.toLong() + return dataSet[position].id } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -140,11 +140,8 @@ class ArtistAdapter( toggleChecked(layoutPosition) } else { image?.let { - ViewCompat.setTransitionName( - it, - activity.getString(R.string.transition_artist_image) - ) - artistClickListener.onArtist(dataSet[layoutPosition].id, it) + ViewCompat.setTransitionName(itemView, "album") + IArtistClickListener.onArtist(dataSet[layoutPosition].id, itemView) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index 40d6bd217..361ba100f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -2,7 +2,6 @@ package code.name.monkey.retromusic.fragments import android.os.Bundle import android.view.View -import android.widget.ImageView import androidx.core.os.bundleOf import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController @@ -16,9 +15,9 @@ import code.name.monkey.retromusic.adapter.artist.ArtistAdapter import code.name.monkey.retromusic.adapter.song.SongAdapter import code.name.monkey.retromusic.db.toSong import code.name.monkey.retromusic.extensions.dipToPix -import code.name.monkey.retromusic.fragments.albums.AlbumClickListener -import code.name.monkey.retromusic.fragments.artists.ArtistClickListener import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment +import code.name.monkey.retromusic.interfaces.IAlbumClickListener +import code.name.monkey.retromusic.interfaces.IArtistClickListener import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.state.NowPlayingPanelState @@ -26,7 +25,7 @@ import code.name.monkey.retromusic.util.RetroUtil import kotlinx.android.synthetic.main.fragment_playlist_detail.* class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_detail), - ArtistClickListener, AlbumClickListener { + IArtistClickListener, IAlbumClickListener { private val args by navArgs() override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -175,12 +174,12 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de return 2 } - override fun onArtist(artistId: Long, imageView: ImageView) { + override fun onArtist(artistId: Long, view: View) { findNavController().navigate( R.id.artistDetailsFragment, bundleOf(EXTRA_ARTIST_ID to artistId), null, - FragmentNavigatorExtras(imageView to getString(R.string.transition_artist_image)) + FragmentNavigatorExtras(view to "artist") ) } @@ -189,7 +188,9 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de R.id.albumDetailsFragment, bundleOf(EXTRA_ALBUM_ID to albumId), null, - FragmentNavigatorExtras(view to getString(R.string.transition_album_art)) + FragmentNavigatorExtras( + view to "album" + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index 1544a6a3c..50030a4a6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -7,6 +7,7 @@ import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.core.text.HtmlCompat +import androidx.core.view.ViewCompat import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController @@ -17,6 +18,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor +import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.EXTRA_ALBUM_ID import code.name.monkey.retromusic.EXTRA_ARTIST_ID @@ -37,6 +39,7 @@ import code.name.monkey.retromusic.glide.RetroMusicColoredTarget import code.name.monkey.retromusic.glide.SingleColorTarget import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.SortOrder +import code.name.monkey.retromusic.interfaces.IAlbumClickListener import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result @@ -48,8 +51,7 @@ import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide -import com.google.android.material.transition.platform.MaterialArcMotion -import com.google.android.material.transition.platform.MaterialContainerTransform +import com.google.android.material.transition.MaterialContainerTransform import kotlinx.android.synthetic.main.fragment_album_content.* import kotlinx.android.synthetic.main.fragment_album_details.* import kotlinx.coroutines.Dispatchers @@ -61,7 +63,7 @@ import org.koin.core.parameter.parametersOf import java.util.* class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_details), - AlbumClickListener { + IAlbumClickListener { private val arguments by navArgs() private val detailsViewModel by viewModel { @@ -79,12 +81,15 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) } + private fun setUpTransitions() { + val transform = MaterialContainerTransform() + transform.setAllContainerColors(ATHUtil.resolveColor(requireContext(), R.attr.colorSurface)) + sharedElementEnterTransition = transform + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - sharedElementEnterTransition = MaterialContainerTransform().apply { - duration = 1000L - pathMotion = MaterialArcMotion() - } + setUpTransitions() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -93,6 +98,7 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) toolbar.title = " " + ViewCompat.setTransitionName(container, "album") postponeEnterTransition() detailsViewModel.getAlbum().observe(viewLifecycleOwner, Observer { startPostponedEnterTransition() @@ -133,7 +139,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det serviceActivity?.removeMusicServiceEventListener(detailsViewModel) } - private fun setupRecyclerView() { simpleSongAdapter = SimpleSongAdapter( requireActivity() as AppCompatActivity, @@ -275,7 +280,9 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det R.id.albumDetailsFragment, bundleOf(EXTRA_ALBUM_ID to albumId), null, - FragmentNavigatorExtras(view to getString(R.string.transition_album_art)) + FragmentNavigatorExtras( + view to "album" + ) ) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt index 96c5b9d81..39c560658 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt @@ -5,21 +5,22 @@ import android.view.* import androidx.core.os.bundleOf import androidx.lifecycle.Observer import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import code.name.monkey.retromusic.EXTRA_ALBUM_ID import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.album.AlbumAdapter -import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewCustomGridSizeFragment import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.SortOrder.AlbumSortOrder +import code.name.monkey.retromusic.interfaces.IAlbumClickListener import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), - AlbumClickListener { + IAlbumClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -98,12 +99,12 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment() private val detailsViewModel: ArtistDetailsViewModel by viewModel { parametersOf(arguments.extraArtistId) @@ -64,20 +67,35 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d private var lang: String? = null private var biography: Spanned? = null + private fun setUpTransitions() { + val transform = MaterialContainerTransform() + transform.setAllContainerColors(ATHUtil.resolveColor(requireContext(), R.attr.colorSurface)) + sharedElementEnterTransition = transform + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpTransitions() + } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + mainActivity.setSupportActionBar(toolbar) + toolbar.title = null + setupRecyclerView() + ViewCompat.setTransitionName(container, "artist") + postponeEnterTransition() detailsViewModel.getArtist().observe(viewLifecycleOwner, Observer { startPostponedEnterTransition() showArtist(it) }) + playAction.apply { setOnClickListener { MusicPlayerRemote.openQueue(artist.songs, 0, true) } } @@ -207,7 +225,7 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d bundleOf(EXTRA_ALBUM_ID to albumId), null, FragmentNavigatorExtras( - view to getString(R.string.transition_album_art) + view to "album" ) ) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt index fd8f3f327..ca9586e61 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistsFragment.kt @@ -2,23 +2,24 @@ package code.name.monkey.retromusic.fragments.artists import android.os.Bundle import android.view.* -import android.widget.ImageView import androidx.core.os.bundleOf import androidx.lifecycle.Observer +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import code.name.monkey.retromusic.EXTRA_ARTIST_ID import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.artist.ArtistAdapter -import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewCustomGridSizeFragment import code.name.monkey.retromusic.helper.SortOrder.ArtistSortOrder +import code.name.monkey.retromusic.interfaces.IArtistClickListener import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment(), - ArtistClickListener { + IArtistClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getArtists().observe(viewLifecycleOwner, Observer { @@ -95,9 +96,13 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment, LM : Recycle protected var adapter: A? = null protected var layoutManager: LM? = null + private fun setUpTransitions() { + exitTransition = Hold() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpTransitions() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index 48c7f42c6..cfc7d3399 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -36,7 +36,6 @@ import code.name.monkey.retromusic.TOP_PLAYED_PLAYLIST import code.name.monkey.retromusic.adapter.HomeAdapter import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog -import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.ProfileBannerGlideRequest import code.name.monkey.retromusic.glide.UserProfileGlideRequest @@ -67,14 +66,14 @@ class HomeFragment : } lastAdded.setOnClickListener { - findActivityNavController(R.id.fragment_container).navigate( + findNavController().navigate( R.id.detailListFragment, bundleOf("type" to LAST_ADDED_PLAYLIST) ) } topPlayed.setOnClickListener { - findActivityNavController(R.id.fragment_container).navigate( + findNavController().navigate( R.id.detailListFragment, bundleOf("type" to TOP_PLAYED_PLAYLIST) ) @@ -85,7 +84,7 @@ class HomeFragment : } history.setOnClickListener { - findActivityNavController(R.id.fragment_container).navigate( + findNavController().navigate( R.id.detailListFragment, bundleOf("type" to HISTORY_PLAYLIST) ) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt index f68d538aa..cc677cf57 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt @@ -8,10 +8,11 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.core.os.bundleOf +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.EXTRA_ARTIST_ID import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.extensions.hide import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.extensions.whichFragment @@ -148,10 +149,12 @@ class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full), private fun setupArtist() { artistImage.setOnClickListener { mainActivity.collapsePanel() - findActivityNavController(R.id.fragment_container) + findNavController() .navigate( R.id.artistDetailsFragment, - bundleOf(EXTRA_ARTIST_ID to MusicPlayerRemote.currentSong.artistId) + bundleOf(EXTRA_ARTIST_ID to MusicPlayerRemote.currentSong.artistId), + null, + FragmentNavigatorExtras(it to "artist") ) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt new file mode 100644 index 000000000..670a07de8 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt @@ -0,0 +1,7 @@ +package code.name.monkey.retromusic.interfaces + +import android.view.View + +interface IAlbumClickListener { + fun onAlbumClick(albumId: Long, view: View) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt new file mode 100644 index 000000000..31e98076e --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt @@ -0,0 +1,7 @@ +package code.name.monkey.retromusic.interfaces + +import android.view.View + +interface IArtistClickListener { + fun onArtist(artistId: Long, view: View) +} \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp/activity_playing_queue.xml b/app/src/main/res/layout-sw600dp/activity_playing_queue.xml index a0da4e0fb..0eddd4025 100644 --- a/app/src/main/res/layout-sw600dp/activity_playing_queue.xml +++ b/app/src/main/res/layout-sw600dp/activity_playing_queue.xml @@ -37,98 +37,16 @@ - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:overScrollMode="never" + android:paddingBottom="96dp" + android:scrollbars="none" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" + tools:listitem="@layout/item_queue" /> - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:overScrollMode="never" + android:paddingBottom="96dp" + android:scrollbars="none" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" + tools:listitem="@layout/item_queue" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_album_details.xml b/app/src/main/res/layout/fragment_album_details.xml index bb2ac3490..32b04d2b6 100644 --- a/app/src/main/res/layout/fragment_album_details.xml +++ b/app/src/main/res/layout/fragment_album_details.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:transitionName="@string/transition_album_art"> + android:layout_height="match_parent"> From f56b158e7824b5dd71811063c7980330bed67423 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 2 Oct 2020 18:18:57 +0530 Subject: [PATCH 30/72] Fixed showing app rate --- .../code/name/monkey/retromusic/util/AppRater.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt b/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt index 8b12c41e0..ded848d20 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt @@ -33,7 +33,7 @@ object AppRater { private const val LAUNCHES_UNTIL_PROMPT = 5//Min number of launches @JvmStatic - fun appLaunched(context: Context) { + fun appLaunched(context: Activity) { val prefs = context.getSharedPreferences(APP_RATING, 0) if (prefs.getBoolean(DO_NOT_SHOW_AGAIN, false)) { return @@ -56,21 +56,24 @@ object AppRater { if (launchCount >= LAUNCHES_UNTIL_PROMPT) { if (System.currentTimeMillis() >= dateFirstLaunch + DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000) { //showRateDialog(context, editor) - showPlayStoreReviewDialog(context) + showPlayStoreReviewDialog(context, editor) } } editor.commit() } - private fun showPlayStoreReviewDialog(context: Context) { + private fun showPlayStoreReviewDialog(context: Activity, editor: SharedPreferences.Editor) { val manager = ReviewManagerFactory.create(context) - manager.requestReviewFlow().addOnCompleteListener { request -> + val flow = manager.requestReviewFlow() + flow.addOnCompleteListener { request -> if (request.isSuccessful) { val reviewInfo = request.result - manager.launchReviewFlow(context as Activity, reviewInfo).addOnCompleteListener { + val flowManager = manager.launchReviewFlow(context, reviewInfo) + flowManager.addOnCompleteListener { if (it.isSuccessful) { - //Toast.makeText(context, "Thanks for the feedback", Toast.LENGTH_SHORT).show() + editor.putBoolean(DO_NOT_SHOW_AGAIN, true) + editor.commit() } } } From 546d974084694c33267059e66d54b563ea4754b8 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 2 Oct 2020 18:26:17 +0530 Subject: [PATCH 31/72] Fix classic theme --- .../activities/base/AbsSlidingMusicPanelActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index f72a11594..f6fda60e8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -25,6 +25,7 @@ import code.name.monkey.retromusic.fragments.player.blur.BlurPlayerFragment import code.name.monkey.retromusic.fragments.player.card.CardFragment import code.name.monkey.retromusic.fragments.player.cardblur.CardBlurFragment import code.name.monkey.retromusic.fragments.player.circle.CirclePlayerFragment +import code.name.monkey.retromusic.fragments.player.classic.ClassicPlayerFragment import code.name.monkey.retromusic.fragments.player.color.ColorFragment import code.name.monkey.retromusic.fragments.player.fit.FitFragment import code.name.monkey.retromusic.fragments.player.flat.FlatPlayerFragment @@ -52,6 +53,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { protected val libraryViewModel by viewModel() private lateinit var bottomSheetBehavior: RetroBottomSheetBehavior + private var playerFragment: AbsPlayerFragment? = null private var miniPlayerFragment: MiniPlayerFragment? = null private var nowPlayingScreen: NowPlayingScreen? = null private var navigationBarColor: Int = 0 @@ -358,7 +360,6 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { }) } - private var playerFragment: AbsPlayerFragment? = null private fun chooseFragmentForTheme() { nowPlayingScreen = PreferenceUtil.nowPlayingScreen @@ -379,6 +380,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { Tiny -> TinyPlayerFragment() Peak -> PeakPlayerFragment() Circle -> CirclePlayerFragment() + Classic -> ClassicPlayerFragment() else -> PlayerFragment() } // must implement AbsPlayerFragment supportFragmentManager.beginTransaction().replace(R.id.playerFragmentContainer, fragment) From 3af3a89ba55f1f3ce8f43fbbbb02385f811d281d Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 2 Oct 2020 23:35:09 +0530 Subject: [PATCH 32/72] Code refactor --- .../retromusic/activities/MainActivity.kt | 2 +- .../base/AbsSlidingMusicPanelActivity.kt | 21 ++++++++------- .../player/classic/ClassicPlayerFragment.kt | 1 - .../main/res/layout/fragment_album_cover.xml | 2 +- .../res/layout/fragment_classic_player.xml | 27 ++++++++++++------- .../res/layout/fragment_main_recycler.xml | 2 +- .../res/layout/sliding_music_panel_layout.xml | 1 + 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index 8c91bf4ef..a9d6bdbbd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -32,7 +32,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis } override fun createContentView(): View { - return wrapSlidingMusicPanel(R.layout.activity_main_content) + return wrapSlidingMusicPanel() } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index f6fda60e8..56b2629cf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -1,15 +1,16 @@ package code.name.monkey.retromusic.activities.base +import android.annotation.SuppressLint import android.graphics.Color import android.os.Bundle import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.FrameLayout -import androidx.annotation.LayoutRes import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R @@ -124,12 +125,13 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallbackList) } - protected fun wrapSlidingMusicPanel(@LayoutRes resId: Int): View { + @SuppressLint("InflateParams") + protected fun wrapSlidingMusicPanel(): View { val slidingMusicPanelLayout = layoutInflater.inflate(R.layout.sliding_music_panel_layout, null) val contentContainer: ViewGroup = slidingMusicPanelLayout.findViewById(R.id.mainContentFrame) - layoutInflater.inflate(resId, contentContainer) + layoutInflater.inflate(R.layout.activity_main_content, contentContainer) return slidingMusicPanelLayout } @@ -334,8 +336,8 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { COLLAPSED_WITH -> { val heightOfBar = bottomNavigationView.height val height = if (isQueueEmpty) 0 else (heightOfBar * 2) - 24 - ViewCompat.setElevation(bottomNavigationView, 10f) - ViewCompat.setElevation(slidingPanel, 10f) + ViewCompat.setElevation(bottomNavigationView, 20f) + ViewCompat.setElevation(slidingPanel, 20f) bottomSheetBehavior.isHideable = false bottomSheetBehavior.peekHeightAnimate(height) bottomNavigationView.translateXAnimate(0f) @@ -383,12 +385,11 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { Classic -> ClassicPlayerFragment() else -> PlayerFragment() } // must implement AbsPlayerFragment - supportFragmentManager.beginTransaction().replace(R.id.playerFragmentContainer, fragment) - .commit() + supportFragmentManager.commit { + replace(R.id.playerFragmentContainer, fragment) + } supportFragmentManager.executePendingTransactions() - - playerFragment = - supportFragmentManager.findFragmentById(R.id.playerFragmentContainer) as AbsPlayerFragment + playerFragment = whichFragment(R.id.playerFragmentContainer) miniPlayerFragment = whichFragment(R.id.miniPlayerFragment) miniPlayerFragment?.view?.setOnClickListener { expandPanel() } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt index c4748128b..f4468695d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/classic/ClassicPlayerFragment.kt @@ -201,7 +201,6 @@ class ClassicPlayerFragment : AbsPlayerFragment(R.layout.fragment_classic_player override fun onPlayStateChanged() { updatePlayPauseDrawableState() - } override fun onRepeatModeChanged() { diff --git a/app/src/main/res/layout/fragment_album_cover.xml b/app/src/main/res/layout/fragment_album_cover.xml index 7f23f04ca..1056385a4 100644 --- a/app/src/main/res/layout/fragment_album_cover.xml +++ b/app/src/main/res/layout/fragment_album_cover.xml @@ -23,7 +23,7 @@ android:layout_height="match_parent" android:scaleType="centerCrop" tools:ignore="ContentDescription,UnusedAttribute" - tools:srcCompat="@tools:sample/backgrounds/scenic[5]" /> + tools:srcCompat="@tools:sample/backgrounds/scenic" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_classic_player.xml b/app/src/main/res/layout/fragment_classic_player.xml index 874e3b392..70f00052a 100644 --- a/app/src/main/res/layout/fragment_classic_player.xml +++ b/app/src/main/res/layout/fragment_classic_player.xml @@ -12,15 +12,22 @@ android:layout_height="match_parent" android:orientation="vertical"> - + app:layout_constraintTop_toTopOf="parent"> + + + + app:layout_constraintTop_toBottomOf="@id/albumCoverContainer" /> + app:cardElevation="24dp" + app:layout_behavior="code.name.monkey.retromusic.RetroBottomSheetBehavior" + tools:peekHeight="0dp"> diff --git a/app/src/main/res/layout/fragment_main_recycler.xml b/app/src/main/res/layout/fragment_main_recycler.xml index 9342abdf6..5a6758477 100644 --- a/app/src/main/res/layout/fragment_main_recycler.xml +++ b/app/src/main/res/layout/fragment_main_recycler.xml @@ -45,7 +45,7 @@ Date: Fri, 2 Oct 2020 23:45:33 +0530 Subject: [PATCH 33/72] Fix small text in Add to playlist dialog --- .../retromusic/dialogs/AddToPlaylistDialog.kt | 32 ++++++++++++------- app/src/main/res/layout/item_simple_text.xml | 9 ++++++ 2 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/layout/item_simple_text.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt index 53acaa254..fc5d6a588 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt @@ -2,6 +2,7 @@ package code.name.monkey.retromusic.dialogs import android.app.Dialog import android.os.Bundle +import android.widget.ArrayAdapter import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope @@ -9,7 +10,6 @@ import code.name.monkey.retromusic.EXTRA_PLAYLISTS import code.name.monkey.retromusic.EXTRA_SONG import code.name.monkey.retromusic.R import code.name.monkey.retromusic.db.PlaylistEntity -import code.name.monkey.retromusic.db.SongEntity import code.name.monkey.retromusic.db.toSongsEntity import code.name.monkey.retromusic.extensions.colorButtons import code.name.monkey.retromusic.extensions.extraNotNull @@ -41,30 +41,40 @@ class AddToPlaylistDialog : DialogFragment() { } } + private fun playlistAdapter(playlists: List): ArrayAdapter { + val adapter = ArrayAdapter(requireContext(), R.layout.item_simple_text, R.id.title) + adapter.addAll(playlists) + return adapter + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val playlistEntities: List = - extraNotNull>(EXTRA_PLAYLISTS).value - val songs: List = extraNotNull>(EXTRA_SONG).value - val playlistNames: MutableList = mutableListOf() + val playlistEntities = extraNotNull>(EXTRA_PLAYLISTS).value + val songs = extraNotNull>(EXTRA_SONG).value + val playlistNames = mutableListOf() playlistNames.add(requireContext().resources.getString(R.string.action_new_playlist)) for (entity: PlaylistEntity in playlistEntities) { playlistNames.add(entity.playlistName) } return materialDialog(R.string.add_playlist_title) - .setItems(playlistNames.toTypedArray()) { _, which -> + .setAdapter( + playlistAdapter(playlistNames) + ) { dialog, which -> if (which == 0) { - CreatePlaylistDialog.create(songs) - .show(requireActivity().supportFragmentManager, "Dialog") + showCreateDialog(songs) } else { lifecycleScope.launch(Dispatchers.IO) { - val songEntities: List = - songs.toSongsEntity(playlistEntities[which - 1]) + val songEntities = songs.toSongsEntity(playlistEntities[which - 1]) libraryViewModel.insertSongs(songEntities) libraryViewModel.forceReload(Playlists) } } - dismiss() + dialog.dismiss() } .create().colorButtons() } + + private fun showCreateDialog(songs: List) { + CreatePlaylistDialog.create(songs).show(requireActivity().supportFragmentManager, "Dialog") + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_simple_text.xml b/app/src/main/res/layout/item_simple_text.xml new file mode 100644 index 000000000..daf36368d --- /dev/null +++ b/app/src/main/res/layout/item_simple_text.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file From c23c56649ed8741c0bf93bd0bcbc5952f009d5a1 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sat, 3 Oct 2020 00:40:26 +0530 Subject: [PATCH 34/72] Code refactor, fix animations on Album and Artist details --- .../base/AbsSlidingMusicPanelActivity.kt | 17 ++-- .../fragments/albums/AlbumDetailsFragment.kt | 11 ++- .../fragments/albums/AlbumsFragment.kt | 87 ++++++++++------ .../fragments/artists/ArtistsFragment.kt | 83 +++++++++++----- .../fragments/songs/SongsFragment.kt | 98 ++++++++++++------- .../monkey/retromusic/util/PreferenceUtil.kt | 2 +- .../layout-land/fragment_album_details.xml | 11 +-- .../layout-land/fragment_artist_details.xml | 10 +- 8 files changed, 205 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 56b2629cf..277040f6d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -148,7 +148,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { miniPlayerFragment?.view?.alpha = alpha miniPlayerFragment?.view?.visibility = if (alpha == 0f) View.GONE else View.VISIBLE bottomNavigationView.translationY = progress * 500 - bottomNavigationView.alpha = alpha + //bottomNavigationView.alpha = alpha } open fun onPanelCollapsed() { @@ -207,8 +207,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { override fun onQueueChanged() { super.onQueueChanged() - val isEmpty = MusicPlayerRemote.playingQueue.isEmpty() - if (isEmpty) { + if (MusicPlayerRemote.playingQueue.isEmpty()) { libraryViewModel.setPanelState(HIDE) } else { if (bottomNavigationView.isVisible) { @@ -229,6 +228,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { collapsePanel() return true } + return false } @@ -323,17 +323,20 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { val isQueueEmpty = MusicPlayerRemote.playingQueue.isEmpty() when (state) { EXPAND -> { + println("EXPAND") expandPanel() } HIDE -> { - ViewCompat.setElevation(slidingPanel, 0f) - ViewCompat.setElevation(bottomNavigationView, 10f) + println("HIDE") + bottomNavigationView.translateXAnimate(0f) bottomSheetBehavior.isHideable = true bottomSheetBehavior.peekHeightAnimate(0) - bottomNavigationView.translateXAnimate(0f) bottomSheetBehavior.state = STATE_COLLAPSED + ViewCompat.setElevation(slidingPanel, 0f) + ViewCompat.setElevation(bottomNavigationView, 10f) } COLLAPSED_WITH -> { + println("COLLAPSED_WITH") val heightOfBar = bottomNavigationView.height val height = if (isQueueEmpty) 0 else (heightOfBar * 2) - 24 ViewCompat.setElevation(bottomNavigationView, 20f) @@ -343,6 +346,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomNavigationView.translateXAnimate(0f) } COLLAPSED_WITHOUT -> { + println("COLLAPSED_WITHOUT") val heightOfBar = bottomNavigationView.height val height = if (isQueueEmpty) 0 else heightOfBar - 24 ViewCompat.setElevation(bottomNavigationView, 10f) @@ -352,6 +356,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomNavigationView.translateXAnimate(150f) } else -> { + println("else") bottomSheetBehavior.isHideable = true bottomSheetBehavior.peekHeight = 0 collapsePanel() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index 50030a4a6..85bd13f20 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -10,7 +10,6 @@ import androidx.core.text.HtmlCompat import androidx.core.view.ViewCompat import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope -import androidx.navigation.findNavController import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -31,6 +30,7 @@ import code.name.monkey.retromusic.dialogs.AddToPlaylistDialog import code.name.monkey.retromusic.dialogs.DeleteSongsDialog import code.name.monkey.retromusic.extensions.applyColor import code.name.monkey.retromusic.extensions.applyOutlineColor +import code.name.monkey.retromusic.extensions.findActivityNavController import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.AlbumGlideRequest @@ -106,11 +106,14 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det }) setupRecyclerView() - artistImage.setOnClickListener { - requireActivity().findNavController(R.id.fragment_container) + artistImage.setOnClickListener { artistView -> + ViewCompat.setTransitionName(artistView, "artist") + findActivityNavController(R.id.fragment_container) .navigate( R.id.artistDetailsFragment, - bundleOf(EXTRA_ARTIST_ID to album.artistId) + bundleOf(EXTRA_ARTIST_ID to album.artistId), + null, + FragmentNavigatorExtras(artistView to "artist") ) } playAction.setOnClickListener { MusicPlayerRemote.openQueue(album.songs, 0, true) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt index 39c560658..621aef7e0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt @@ -10,17 +10,21 @@ import androidx.recyclerview.widget.GridLayoutManager import code.name.monkey.retromusic.EXTRA_ALBUM_ID import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.album.AlbumAdapter +import code.name.monkey.retromusic.extensions.surfaceColor import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewCustomGridSizeFragment import code.name.monkey.retromusic.helper.SortOrder import code.name.monkey.retromusic.helper.SortOrder.AlbumSortOrder import code.name.monkey.retromusic.interfaces.IAlbumClickListener +import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.RetroColorUtil import code.name.monkey.retromusic.util.RetroUtil +import com.afollestad.materialcab.MaterialCab class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), - IAlbumClickListener { + IAlbumClickListener, ICabHolder { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -45,7 +49,7 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment sortOrder = AlbumSortOrder.ALBUM_A_Z - R.id.action_album_sort_order_desc -> sortOrder = AlbumSortOrder.ALBUM_Z_A - R.id.action_album_sort_order_artist -> sortOrder = AlbumSortOrder.ALBUM_ARTIST - R.id.action_album_sort_order_year -> sortOrder = AlbumSortOrder.ALBUM_YEAR + val sortOrder: String = when (item.itemId) { + R.id.action_album_sort_order_asc -> AlbumSortOrder.ALBUM_A_Z + R.id.action_album_sort_order_desc -> AlbumSortOrder.ALBUM_Z_A + R.id.action_album_sort_order_artist -> AlbumSortOrder.ALBUM_ARTIST + R.id.action_album_sort_order_year -> AlbumSortOrder.ALBUM_YEAR + else -> PreferenceUtil.albumSortOrder } - if (sortOrder != null) { + if (sortOrder != PreferenceUtil.albumSortOrder) { item.isChecked = true setAndSaveSortOrder(sortOrder) return true @@ -244,16 +247,16 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment layoutRes = R.layout.item_grid - R.id.action_layout_card -> layoutRes = R.layout.item_card - R.id.action_layout_colored_card -> layoutRes = R.layout.item_card_color - R.id.action_layout_circular -> layoutRes = R.layout.item_grid_circle - R.id.action_layout_image -> layoutRes = R.layout.image - R.id.action_layout_gradient_image -> layoutRes = R.layout.item_image_gradient + val layoutRes = when (item.itemId) { + R.id.action_layout_normal -> R.layout.item_grid + R.id.action_layout_card -> R.layout.item_card + R.id.action_layout_colored_card -> R.layout.item_card_color + R.id.action_layout_circular -> R.layout.item_grid_circle + R.id.action_layout_image -> R.layout.image + R.id.action_layout_gradient_image -> R.layout.item_image_gradient + else -> PreferenceUtil.albumGridStyle } - if (layoutRes != -1) { + if (layoutRes != PreferenceUtil.albumGridStyle) { item.isChecked = true setAndSaveLayoutRes(layoutRes) return true @@ -264,16 +267,16 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment gridSize = 1 - R.id.action_grid_size_2 -> gridSize = 2 - R.id.action_grid_size_3 -> gridSize = 3 - R.id.action_grid_size_4 -> gridSize = 4 - R.id.action_grid_size_5 -> gridSize = 5 - R.id.action_grid_size_6 -> gridSize = 6 - R.id.action_grid_size_7 -> gridSize = 7 - R.id.action_grid_size_8 -> gridSize = 8 + val gridSize = when (item.itemId) { + R.id.action_grid_size_1 -> 1 + R.id.action_grid_size_2 -> 2 + R.id.action_grid_size_3 -> 3 + R.id.action_grid_size_4 -> 4 + R.id.action_grid_size_5 -> 5 + R.id.action_grid_size_6 -> 6 + R.id.action_grid_size_7 -> 7 + R.id.action_grid_size_8 -> 8 + else -> 0 } if (gridSize > 0) { item.isChecked = true @@ -283,4 +286,30 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), - IArtistClickListener { + IArtistClickListener, ICabHolder { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getArtists().observe(viewLifecycleOwner, Observer { @@ -47,7 +51,7 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment sortOrder = ArtistSortOrder.ARTIST_A_Z - R.id.action_artist_sort_order_desc -> sortOrder = ArtistSortOrder.ARTIST_Z_A + val sortOrder: String = when (item.itemId) { + R.id.action_artist_sort_order_asc -> ArtistSortOrder.ARTIST_A_Z + R.id.action_artist_sort_order_desc -> ArtistSortOrder.ARTIST_Z_A + else -> PreferenceUtil.artistSortOrder } - if (sortOrder != null) { + if (sortOrder != PreferenceUtil.artistSortOrder) { item.isChecked = true setAndSaveSortOrder(sortOrder) return true @@ -222,16 +225,16 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment layoutRes = R.layout.item_grid - R.id.action_layout_card -> layoutRes = R.layout.item_card - R.id.action_layout_colored_card -> layoutRes = R.layout.item_card_color - R.id.action_layout_circular -> layoutRes = R.layout.item_grid_circle - R.id.action_layout_image -> layoutRes = R.layout.image - R.id.action_layout_gradient_image -> layoutRes = R.layout.item_image_gradient + val layoutRes = when (item.itemId) { + R.id.action_layout_normal -> R.layout.item_grid + R.id.action_layout_card -> R.layout.item_card + R.id.action_layout_colored_card -> R.layout.item_card_color + R.id.action_layout_circular -> R.layout.item_grid_circle + R.id.action_layout_image -> R.layout.image + R.id.action_layout_gradient_image -> R.layout.item_image_gradient + else -> PreferenceUtil.artistGridStyle } - if (layoutRes != -1) { + if (layoutRes != PreferenceUtil.artistGridStyle) { item.isChecked = true setAndSaveLayoutRes(layoutRes) return true @@ -242,16 +245,16 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment gridSize = 1 - R.id.action_grid_size_2 -> gridSize = 2 - R.id.action_grid_size_3 -> gridSize = 3 - R.id.action_grid_size_4 -> gridSize = 4 - R.id.action_grid_size_5 -> gridSize = 5 - R.id.action_grid_size_6 -> gridSize = 6 - R.id.action_grid_size_7 -> gridSize = 7 - R.id.action_grid_size_8 -> gridSize = 8 + val gridSize = when (item.itemId) { + R.id.action_grid_size_1 -> 1 + R.id.action_grid_size_2 -> 2 + R.id.action_grid_size_3 -> 3 + R.id.action_grid_size_4 -> 4 + R.id.action_grid_size_5 -> 5 + R.id.action_grid_size_6 -> 6 + R.id.action_grid_size_7 -> 7 + R.id.action_grid_size_8 -> 8 + else -> 0 } if (gridSize > 0) { item.isChecked = true @@ -260,4 +263,30 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment() { +class SongsFragment : AbsRecyclerViewCustomGridSizeFragment(), + ICabHolder { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) libraryViewModel.getSongs().observe(viewLifecycleOwner, Observer { @@ -38,7 +43,7 @@ class SongsFragment : AbsRecyclerViewCustomGridSizeFragment sortOrder = SongSortOrder.SONG_A_Z - R.id.action_song_sort_order_desc -> sortOrder = SongSortOrder.SONG_Z_A - R.id.action_song_sort_order_artist -> sortOrder = SongSortOrder.SONG_ARTIST - R.id.action_song_sort_order_album -> sortOrder = SongSortOrder.SONG_ALBUM - R.id.action_song_sort_order_year -> sortOrder = SongSortOrder.SONG_YEAR - R.id.action_song_sort_order_date -> sortOrder = SongSortOrder.SONG_DATE - R.id.action_song_sort_order_composer -> sortOrder = SongSortOrder.COMPOSER - R.id.action_song_sort_order_date_modified -> sortOrder = - SongSortOrder.SONG_DATE_MODIFIED + val sortOrder: String = when (item.itemId) { + R.id.action_song_sort_order_asc -> SongSortOrder.SONG_A_Z + R.id.action_song_sort_order_desc -> SongSortOrder.SONG_Z_A + R.id.action_song_sort_order_artist -> SongSortOrder.SONG_ARTIST + R.id.action_song_sort_order_album -> SongSortOrder.SONG_ALBUM + R.id.action_song_sort_order_year -> SongSortOrder.SONG_YEAR + R.id.action_song_sort_order_date -> SongSortOrder.SONG_DATE + R.id.action_song_sort_order_composer -> SongSortOrder.COMPOSER + R.id.action_song_sort_order_date_modified -> SongSortOrder.SONG_DATE_MODIFIED + else -> PreferenceUtil.songSortOrder } - if (sortOrder != null) { + if (sortOrder != PreferenceUtil.songSortOrder) { item.isChecked = true setAndSaveSortOrder(sortOrder) return true @@ -251,16 +254,16 @@ class SongsFragment : AbsRecyclerViewCustomGridSizeFragment layoutRes = R.layout.item_grid - R.id.action_layout_card -> layoutRes = R.layout.item_card - R.id.action_layout_colored_card -> layoutRes = R.layout.item_card_color - R.id.action_layout_circular -> layoutRes = R.layout.item_grid_circle - R.id.action_layout_image -> layoutRes = R.layout.image - R.id.action_layout_gradient_image -> layoutRes = R.layout.item_image_gradient + val layoutRes = when (item.itemId) { + R.id.action_layout_normal -> R.layout.item_grid + R.id.action_layout_card -> R.layout.item_card + R.id.action_layout_colored_card -> R.layout.item_card_color + R.id.action_layout_circular -> R.layout.item_grid_circle + R.id.action_layout_image -> R.layout.image + R.id.action_layout_gradient_image -> R.layout.item_image_gradient + else -> PreferenceUtil.songGridStyle } - if (layoutRes != -1) { + if (layoutRes != PreferenceUtil.songGridStyle) { item.isChecked = true setAndSaveLayoutRes(layoutRes) return true @@ -271,16 +274,16 @@ class SongsFragment : AbsRecyclerViewCustomGridSizeFragment gridSize = 1 - R.id.action_grid_size_2 -> gridSize = 2 - R.id.action_grid_size_3 -> gridSize = 3 - R.id.action_grid_size_4 -> gridSize = 4 - R.id.action_grid_size_5 -> gridSize = 5 - R.id.action_grid_size_6 -> gridSize = 6 - R.id.action_grid_size_7 -> gridSize = 7 - R.id.action_grid_size_8 -> gridSize = 8 + val gridSize = when (item.itemId) { + R.id.action_grid_size_1 -> 1 + R.id.action_grid_size_2 -> 2 + R.id.action_grid_size_3 -> 3 + R.id.action_grid_size_4 -> 4 + R.id.action_grid_size_5 -> 5 + R.id.action_grid_size_6 -> 6 + R.id.action_grid_size_7 -> 7 + R.id.action_grid_size_8 -> 8 + else -> 0 } if (gridSize > 0) { item.isChecked = true @@ -299,4 +302,31 @@ class SongsFragment : AbsRecyclerViewCustomGridSizeFragment LabelVisibilityMode.LABEL_VISIBILITY_LABELED 0 -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO diff --git a/app/src/main/res/layout-land/fragment_album_details.xml b/app/src/main/res/layout-land/fragment_album_details.xml index 40e2d12d8..30917f459 100644 --- a/app/src/main/res/layout-land/fragment_album_details.xml +++ b/app/src/main/res/layout-land/fragment_album_details.xml @@ -2,6 +2,7 @@ - - @@ -53,9 +51,8 @@ - - @@ -53,9 +52,8 @@ - Date: Sat, 3 Oct 2020 17:49:21 +0530 Subject: [PATCH 35/72] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 66b64c0dc..25c177cb1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Dark and Just Black for AMOLED displays. Select your favorite accent color from a color palette. ### 🏠 Home -Where you can have your recently/top played artists, albums and +Where you can view your recently/top played artists, albums and favorite songs. No other music player has this feature. ### 📦 Included Features @@ -49,17 +49,17 @@ favorite songs. No other music player has this feature. - Folder support - Play song by folder - Gapless playback - Volume controls -- Carousel effect for an album cover +- Carousel effect for album covers - Home screen widgets - Lock screen playback controls - Lyrics screen (download and sync with music) -- Sleep Timer +- Sleep timer - Easy drag to sort playlist & play queue - Tag editor - Create, edit and import playlists - Playing queue with reorder - User profile -- 30 Languages support +- 30+ languages support - Browse and play your music by songs, albums, artists, playlists and genre - Smart Auto Playlists - Recently played, most played and history @@ -69,7 +69,7 @@ favorite songs. No other music player has this feature. We are trying our best to bring you the best user experience. The app is regulary being updated for bug fixes and new features. ### ❓ FAQ -Please read the FAQ here: https://del.dog/RetroFaq +Please read the FAQ [here](https://github.com/h4h13/RetroMusicPlayer/blob/dev/FAQ.md) In any case, you find or notice any bugs please report them by sending us an [e-mail](mailto:retromusicapp@gmail.com). We will fix bugs as soon as @@ -85,7 +85,7 @@ If you have any feature suggestions, please create an issue with detailed inform ### 🗂️ License Retro Music Player is released under the GNU General Public License v3.0 -(GPLv3), which can be found here: [License](LICENSE.md) +(GPLv3), which can be found [here](LICENSE.md) >Please note: Retro Music player is an offline music player app. It From 1631449c130c2c0501a600f2a7098c34c85cb007 Mon Sep 17 00:00:00 2001 From: "Daksh P. Jain" Date: Sat, 3 Oct 2020 19:26:21 +0530 Subject: [PATCH 36/72] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25c177cb1..e99cf7066 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ favorite songs. No other music player has this feature. - Driving Mode - Headset/Bluetooth support - Music duration filter -- Folder support - Play song by folder +- Folder support - Play songs by folder - Gapless playback - Volume controls - Carousel effect for album covers @@ -63,10 +63,10 @@ favorite songs. No other music player has this feature. - Browse and play your music by songs, albums, artists, playlists and genre - Smart Auto Playlists - Recently played, most played and history -- Build your own playlist on the go +- Build your playlist on the go -We are trying our best to bring you the best user experience. The app is regulary being updated for bug fixes and new features. +We are trying our best to bring you the best user experience. The app is regularly being updated for bug fixes and new features. ### ❓ FAQ Please read the FAQ [here](https://github.com/h4h13/RetroMusicPlayer/blob/dev/FAQ.md) @@ -88,5 +88,5 @@ Retro Music Player is released under the GNU General Public License v3.0 (GPLv3), which can be found [here](LICENSE.md) ->Please note: Retro Music player is an offline music player app. It +>Please note: Retro Music Player is an offline music player app. It >doesn't support music downloading or online music streaming. From f02888113e98493c479093890264e06fadf09233 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 4 Oct 2020 14:43:42 +0530 Subject: [PATCH 37/72] Code fixes --- .../name/monkey/retromusic/adapter/song/SongAdapter.kt | 2 +- .../fragments/playlists/PlaylistDetailsViewModel.kt | 10 +++------- .../name/monkey/retromusic/repository/Repository.kt | 6 +++--- .../monkey/retromusic/repository/RoomRepository.kt | 6 +++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index f5c18a47b..a6b6fe66b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -60,7 +60,7 @@ open class SongAdapter( } override fun getItemId(position: Int): Long { - return dataSet[position].id.toLong() + return dataSet[position].id } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt index d35a90718..2c1fa879e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt @@ -14,14 +14,10 @@ class PlaylistDetailsViewModel( private var playlist: PlaylistWithSongs ) : ViewModel(), IMusicServiceEventListener { - private val _playListSongs = MutableLiveData>() - private val _playlist = MutableLiveData().apply { - postValue(playlist) - } + private val playListSongs = MutableLiveData>() - fun getPlaylist(): LiveData = _playlist - - fun getSongs(): LiveData> = realRepository.playlistSongs(playlist.playlistEntity) + fun getSongs(): LiveData> = + realRepository.playlistSongs(playlist.playlistEntity.playListId) override fun onMediaStoreChanged() { diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt index 476dac747..cbd29a90a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/Repository.kt @@ -42,7 +42,7 @@ interface Repository { fun favorites(): LiveData> fun observableHistorySongs(): LiveData> fun albumById(albumId: Long): Album - fun playlistSongs(playlistEntity: PlaylistEntity): LiveData> + fun playlistSongs(playListId: Long): LiveData> suspend fun fetchAlbums(): List suspend fun albumByIdAsync(albumId: Long): Album suspend fun allSongs(): List @@ -252,8 +252,8 @@ class RealRepository( it.toSong() } - override fun playlistSongs(playlistEntity: PlaylistEntity): LiveData> = - roomRepository.getSongs(playlistEntity) + override fun playlistSongs(playListId: Long): LiveData> = + roomRepository.getSongs(playListId) override suspend fun insertSongs(songs: List) = roomRepository.insertSongs(songs) diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt index 4a6eb42d2..ca8a9833d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt @@ -11,7 +11,7 @@ interface RoomRepository { fun favoritePlaylistLiveData(favorite: String): LiveData> fun insertBlacklistPath(blackListStoreEntity: BlackListStoreEntity) fun observableHistorySongs(): LiveData> - fun getSongs(playlistEntity: PlaylistEntity): LiveData> + fun getSongs(playListId: Long): LiveData> suspend fun createPlaylist(playlistEntity: PlaylistEntity): Long suspend fun checkPlaylistExists(playlistName: String): List suspend fun playlists(): List @@ -70,8 +70,8 @@ class RealRoomRepository( } - override fun getSongs(playlistEntity: PlaylistEntity): LiveData> = - playlistDao.songsFromPlaylist(playlistEntity.playListId) + override fun getSongs(playListId: Long): LiveData> = + playlistDao.songsFromPlaylist(playListId) override suspend fun deletePlaylistEntities(playlistEntities: List) = playlistDao.deletePlaylists(playlistEntities) From 6fd3f36e317a15bfe92892ec4a29d4b06977ed25 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 4 Oct 2020 19:01:40 +0530 Subject: [PATCH 38/72] Modified User details --- app/src/debug/res/values/styles.xml | 5 + .../retromusic/activities/UserInfoActivity.kt | 2 +- .../main/res/layout/activity_user_info.xml | 95 +++++-------------- .../main/res/layout/fragment_banner_home.xml | 2 +- 4 files changed, 32 insertions(+), 72 deletions(-) diff --git a/app/src/debug/res/values/styles.xml b/app/src/debug/res/values/styles.xml index 81154a395..dcee15a66 100644 --- a/app/src/debug/res/values/styles.xml +++ b/app/src/debug/res/values/styles.xml @@ -88,4 +88,9 @@ 0.0125 ?android:attr/textColorPrimary + + \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt index a97642f4e..eda1e1b03 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt @@ -55,7 +55,7 @@ class UserInfoActivity : AbsBaseActivity() { pickNewPhoto() } - bannerSelect.setOnClickListener { + bannerImage.setOnClickListener { selectBannerImage() } diff --git a/app/src/main/res/layout/activity_user_info.xml b/app/src/main/res/layout/activity_user_info.xml index 154e4234f..f99937744 100644 --- a/app/src/main/res/layout/activity_user_info.xml +++ b/app/src/main/res/layout/activity_user_info.xml @@ -25,95 +25,50 @@ - + app:layout_constraintTop_toTopOf="parent" + app:shapeAppearanceOverlay="@style/circleImageView" + app:srcCompat="@drawable/material_design_default" /> - - - - - - - - - - - - - - - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/bannerImage" + app:retroCornerSize="36dp" + app:srcCompat="@drawable/ic_person_flat" /> + app:layout_constraintStart_toEndOf="@id/userImage" + app:layout_constraintTop_toTopOf="@id/userImage"> + tools:srcCompat="@tools:sample/backgrounds/scenic" /> Date: Tue, 6 Oct 2020 09:40:16 +0530 Subject: [PATCH 39/72] Artist fallback Fallback to fetch Album cover for missing artists --- .../retromusic/glide/ArtistGlideRequest.java | 2 +- .../glide/artistimage/ArtistImageLoader.kt | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java index 4d9d2124b..c11ffcc2d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java @@ -62,7 +62,7 @@ public class ArtistGlideRequest { boolean hasCustomImage = CustomArtistImageUtil.Companion.getInstance(App.Companion.getContext()) .hasCustomArtistImage(artist); if (noCustomImage || !hasCustomImage) { - return requestManager.load(new ArtistImage(artist.getName())); + return requestManager.load(new ArtistImage(artist)); } else { return requestManager.load(CustomArtistImageUtil.getFile(artist)); } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index 43d23120d..9b67cb628 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -15,6 +15,7 @@ package code.name.monkey.retromusic.glide.artistimage import android.content.Context +import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Data import code.name.monkey.retromusic.network.DeezerService import code.name.monkey.retromusic.util.MusicUtil @@ -34,7 +35,7 @@ import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit -class ArtistImage(val artistName: String) +class ArtistImage(val artist: Artist) class ArtistImageFetcher( private val context: Context, @@ -53,7 +54,7 @@ class ArtistImageFetcher( } override fun getId(): String { - return model.artistName + return model.artist.name } override fun cancel() { @@ -62,10 +63,10 @@ class ArtistImageFetcher( } override fun loadData(priority: Priority?): InputStream? { - if (!MusicUtil.isArtistNameUnknown(model.artistName) && + if (!MusicUtil.isArtistNameUnknown(model.artist.name) && PreferenceUtil.isAllowedToDownloadMetadata() ) { - val artists = model.artistName.split(",") + val artists = model.artist.name.split(",") val response = deezerService.getArtistImage(artists[0]).execute() if (!response.isSuccessful) { @@ -85,13 +86,21 @@ class ArtistImageFetcher( val glideUrl = GlideUrl(imageUrl) urlFetcher = urlLoader.getResourceFetcher(glideUrl, width, height) urlFetcher?.loadData(priority) - } else null + } else { + getFallbackAlbumImage() + } } catch (e: Exception) { - null + getFallbackAlbumImage() } } else return null } + private fun getFallbackAlbumImage(): InputStream? { + val imageUri = MusicUtil.getMediaStoreAlbumCoverUri(model.artist.safeGetFirstAlbum().id) + return context.contentResolver.openInputStream(imageUri) + } + + private fun getHighestQuality(imageUrl: Data): String { return when { imageUrl.pictureXl.isNotEmpty() -> imageUrl.pictureXl From defcd86152b01362508524e3a0e159aff518d259 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 6 Oct 2020 14:16:04 +0530 Subject: [PATCH 40/72] Add Spotless --- app/build.gradle | 4 +- .../java/code/name/monkey/retromusic/App.kt | 10 +- .../code/name/monkey/retromusic/Constants.kt | 18 +- .../name/monkey/retromusic/HomeSection.kt | 16 +- .../retromusic/LanguageContextWrapper.java | 58 +- .../retromusic/RetroBottomSheetBehavior.java | 44 +- .../activities/DriveModeActivity.kt | 12 +- .../activities/LicenseActivity.java | 126 +- .../activities/LockScreenActivity.kt | 16 +- .../retromusic/activities/LyricsActivity.kt | 23 +- .../retromusic/activities/MainActivity.kt | 24 +- .../activities/PermissionActivity.kt | 19 +- .../activities/PlayingQueueActivity.kt | 16 +- .../retromusic/activities/PurchaseActivity.kt | 17 +- .../retromusic/activities/SettingsActivity.kt | 17 +- .../activities/ShareInstagramStory.kt | 10 +- .../activities/SupportDevelopmentActivity.kt | 25 +- .../retromusic/activities/UserInfoActivity.kt | 26 +- .../activities/WhatsNewActivity.java | 151 +- .../activities/base/AbsBaseActivity.kt | 18 +- .../base/AbsMusicServiceActivity.kt | 25 +- .../base/AbsSlidingMusicPanelActivity.kt | 20 +- .../activities/base/AbsThemeActivity.kt | 17 +- .../activities/bugreport/BugReportActivity.kt | 17 +- .../bugreport/model/DeviceInfo.java | 260 +- .../activities/bugreport/model/Report.java | 41 +- .../bugreport/model/github/ExtraInfo.java | 97 +- .../bugreport/model/github/GithubLogin.java | 51 +- .../bugreport/model/github/GithubTarget.java | 24 +- .../activities/saf/SAFGuideActivity.java | 85 +- .../tageditor/AbsTagEditorActivity.kt | 22 +- .../tageditor/AlbumTagEditorActivity.kt | 18 +- .../tageditor/SongTagEditorActivity.kt | 18 +- .../tageditor/WriteTagsAsyncTask.java | 312 +-- .../adapter/CategoryInfoAdapter.java | 173 +- .../retromusic/adapter/ContributorAdapter.kt | 14 + .../monkey/retromusic/adapter/GenreAdapter.kt | 14 + .../monkey/retromusic/adapter/HomeAdapter.kt | 16 +- .../retromusic/adapter/SearchAdapter.kt | 18 +- .../retromusic/adapter/SongFileAdapter.kt | 24 +- .../retromusic/adapter/TranslatorsAdapter.kt | 16 +- .../retromusic/adapter/album/AlbumAdapter.kt | 17 +- .../adapter/album/AlbumCoverPagerAdapter.kt | 15 +- .../adapter/album/HorizontalAlbumAdapter.kt | 18 +- .../adapter/artist/ArtistAdapter.kt | 19 +- .../adapter/base/AbsMultiSelectAdapter.java | 197 +- .../adapter/base/MediaEntryViewHolder.java | 152 +- .../adapter/playlist/LegacyPlaylistAdapter.kt | 16 +- .../adapter/playlist/PlaylistAdapter.kt | 18 +- .../adapter/song/AbsOffsetSongAdapter.kt | 16 +- .../song/OrderablePlaylistSongAdapter.kt | 14 + .../adapter/song/PlayingQueueAdapter.kt | 22 +- .../adapter/song/PlaylistSongAdapter.kt | 16 +- .../adapter/song/ShuffleButtonSongAdapter.kt | 16 +- .../adapter/song/SimpleSongAdapter.kt | 14 + .../retromusic/adapter/song/SongAdapter.kt | 14 + .../appshortcuts/AppShortcutIconGenerator.kt | 15 +- .../AppShortcutLauncherActivity.kt | 10 +- .../appshortcuts/DynamicShortcutManager.kt | 14 +- .../shortcuttype/BaseShortcutType.kt | 10 +- .../shortcuttype/LastAddedShortcutType.kt | 10 +- .../shortcuttype/ShuffleAllShortcutType.kt | 10 +- .../shortcuttype/TopTracksShortcutType.kt | 10 +- .../retromusic/appwidgets/AppWidgetBig.kt | 11 +- .../retromusic/appwidgets/AppWidgetCard.kt | 10 +- .../retromusic/appwidgets/AppWidgetClassic.kt | 11 +- .../retromusic/appwidgets/AppWidgetSmall.kt | 10 +- .../retromusic/appwidgets/AppWidgetText.kt | 13 +- .../retromusic/appwidgets/BootReceiver.kt | 10 +- .../appwidgets/base/BaseAppWidget.kt | 28 +- .../monkey/retromusic/db/BlackListStoreDao.kt | 16 +- .../retromusic/db/BlackListStoreEntity.kt | 16 +- .../name/monkey/retromusic/db/HistoryDao.kt | 16 +- .../monkey/retromusic/db/HistoryEntity.kt | 16 +- .../name/monkey/retromusic/db/LyricsDao.kt | 16 +- .../name/monkey/retromusic/db/LyricsEntity.kt | 16 +- .../name/monkey/retromusic/db/PlayCountDao.kt | 16 +- .../monkey/retromusic/db/PlayCountEntity.kt | 16 +- .../name/monkey/retromusic/db/PlaylistDao.kt | 19 +- .../monkey/retromusic/db/PlaylistEntity.kt | 16 +- .../monkey/retromusic/db/PlaylistWithSongs.kt | 15 +- .../monkey/retromusic/db/RetroDatabase.kt | 16 +- .../name/monkey/retromusic/db/SongEntity.kt | 15 +- .../monkey/retromusic/db/SongExtension.kt | 15 +- .../retromusic/dialogs/AddToPlaylistDialog.kt | 17 +- .../dialogs/BlacklistFolderChooserDialog.java | 247 +- .../dialogs/CreatePlaylistDialog.kt | 16 +- .../dialogs/DeletePlaylistDialog.kt | 17 +- .../retromusic/dialogs/DeleteSongsDialog.kt | 16 +- .../dialogs/ImportPlaylistDialog.kt | 16 +- .../dialogs/RemoveSongFromPlaylistDialog.kt | 16 +- .../dialogs/RenamePlaylistDialog.kt | 16 +- .../retromusic/dialogs/SavePlaylistDialog.kt | 18 +- .../retromusic/dialogs/SleepTimerDialog.kt | 13 +- .../retromusic/dialogs/SongDetailDialog.kt | 14 +- .../retromusic/dialogs/SongShareDialog.kt | 10 +- .../retromusic/extensions/ActivityEx.kt | 12 +- .../monkey/retromusic/extensions/ColorExt.kt | 12 +- .../retromusic/extensions/CursorExtensions.kt | 16 +- .../retromusic/extensions/DialogExtension.kt | 16 +- .../retromusic/extensions/DimenExtension.kt | 16 +- .../retromusic/extensions/DrawableExt.kt | 12 +- .../retromusic/extensions/FragmentExt.kt | 18 +- .../extensions/NavigationExtensions.kt | 16 +- .../monkey/retromusic/extensions/PaletteEX.kt | 15 +- .../retromusic/extensions/Preference.kt | 14 + .../retromusic/extensions/ViewExtensions.kt | 14 +- .../retromusic/fragments/AlbumCoverStyle.kt | 15 +- .../fragments/CoroutineViewModel.kt | 18 +- .../fragments/DetailListFragment.kt | 16 +- .../retromusic/fragments/LibraryViewModel.kt | 17 +- .../fragments/MiniPlayerFragment.kt | 29 +- .../retromusic/fragments/NowPlayingScreen.kt | 14 + .../retromusic/fragments/VolumeFragment.kt | 21 +- .../fragments/about/AboutFragment.kt | 15 +- .../fragments/albums/AlbumDetailsFragment.kt | 18 +- .../fragments/albums/AlbumDetailsViewModel.kt | 17 +- .../fragments/albums/AlbumsFragment.kt | 16 +- .../artists/ArtistDetailsFragment.kt | 23 +- .../artists/ArtistDetailsViewModel.kt | 16 +- .../fragments/artists/ArtistsFragment.kt | 18 +- .../fragments/base/AbsMainActivityFragment.kt | 14 + .../fragments/base/AbsMusicServiceFragment.kt | 16 +- .../base/AbsPlayerControlsFragment.kt | 15 +- .../fragments/base/AbsPlayerFragment.kt | 17 +- .../AbsRecyclerViewCustomGridSizeFragment.kt | 17 +- .../fragments/base/AbsRecyclerViewFragment.kt | 18 +- .../fragments/folder/FoldersFragment.java | 1340 ++++----- .../fragments/genres/GenreDetailsFragment.kt | 18 +- .../fragments/genres/GenreDetailsViewModel.kt | 16 +- .../fragments/genres/GenresFragment.kt | 13 +- .../retromusic/fragments/home/HomeFragment.kt | 12 +- .../fragments/library/LibraryFragment.kt | 16 +- .../player/PlayerAlbumCoverFragment.kt | 16 +- .../player/adaptive/AdaptiveFragment.kt | 16 +- .../AdaptivePlaybackControlsFragment.kt | 22 +- .../blur/BlurPlaybackControlsFragment.kt | 14 + .../player/blur/BlurPlayerFragment.kt | 16 +- .../fragments/player/card/CardFragment.kt | 14 + .../card/CardPlaybackControlsFragment.kt | 21 +- .../player/cardblur/CardBlurFragment.kt | 15 +- .../CardBlurPlaybackControlsFragment.kt | 14 + .../player/circle/CirclePlayerFragment.kt | 14 +- .../player/classic/ClassicPlayerFragment.kt | 24 +- .../fragments/player/color/ColorFragment.kt | 14 + .../color/ColorPlaybackControlsFragment.kt | 15 +- .../fragments/player/fit/FitFragment.kt | 14 + .../player/fit/FitPlaybackControlsFragment.kt | 17 +- .../flat/FlatPlaybackControlsFragment.kt | 14 + .../player/flat/FlatPlayerFragment.kt | 15 +- .../full/FullPlaybackControlsFragment.kt | 17 +- .../player/full/FullPlayerFragment.kt | 16 +- .../player/gradient/GradientPlayerFragment.kt | 20 +- .../player/home/HomePlayerFragment.kt | 21 +- .../lockscreen/LockScreenControlsFragment.kt | 12 +- .../material/MaterialControlsFragment.kt | 14 + .../player/material/MaterialFragment.kt | 14 + .../fragments/player/normal/PlayerFragment.kt | 19 +- .../normal/PlayerPlaybackControlsFragment.kt | 14 + .../player/peak/PeakPlayerControlFragment.kt | 14 +- .../player/peak/PeakPlayerFragment.kt | 13 +- .../plain/PlainPlaybackControlsFragment.kt | 15 +- .../player/plain/PlainPlayerFragment.kt | 14 + .../simple/SimplePlaybackControlsFragment.kt | 14 + .../player/simple/SimplePlayerFragment.kt | 15 +- .../tiny/TinyPlaybackControlsFragment.kt | 18 +- .../player/tiny/TinyPlayerFragment.kt | 20 +- .../playlists/PlaylistDetailsViewModel.kt | 15 +- .../fragments/playlists/PlaylistsFragment.kt | 14 + .../fragments/queue/PlayingQueueFragment.kt | 11 +- .../fragments/search/SearchFragment.kt | 23 +- .../fragments/settings/AbsSettingsFragment.kt | 10 +- .../fragments/settings/AudioSettings.kt | 11 +- .../settings/ImageSettingFragment.kt | 10 +- .../settings/MainSettingsFragment.kt | 15 +- .../settings/NotificationSettingsFragment.kt | 11 +- .../settings/NowPlayingSettingsFragment.kt | 10 +- .../settings/OtherSettingsFragment.kt | 10 +- .../settings/PersonalizeSettingsFragment.kt | 10 +- .../settings/ThemeSettingsFragment.kt | 11 +- .../fragments/songs/SongsFragment.kt | 15 +- .../retromusic/glide/AlbumGlideRequest.java | 211 +- .../retromusic/glide/ArtistGlideRequest.java | 249 +- .../retromusic/glide/BlurTransformation.kt | 15 +- .../glide/ProfileBannerGlideRequest.java | 111 +- .../glide/RetroMusicColoredTarget.kt | 11 +- .../retromusic/glide/RetroMusicGlideModule.kt | 10 +- .../retromusic/glide/SingleColorTarget.kt | 17 +- .../retromusic/glide/SongGlideRequest.java | 209 +- .../glide/UserProfileGlideRequest.java | 119 +- .../glide/artistimage/ArtistImageLoader.kt | 19 +- .../glide/audiocover/AudioFileCover.java | 12 +- .../audiocover/AudioFileCoverFetcher.java | 89 +- .../audiocover/AudioFileCoverLoader.java | 28 +- .../glide/audiocover/AudioFileCoverUtils.java | 70 +- .../glide/palette/BitmapPaletteResource.java | 38 +- .../glide/palette/BitmapPaletteTarget.java | 15 +- .../palette/BitmapPaletteTranscoder.java | 41 +- .../glide/palette/BitmapPaletteWrapper.java | 25 +- .../helper/HorizontalAdapterHelper.kt | 14 +- .../retromusic/helper/M3UConstants.java | 10 +- .../monkey/retromusic/helper/M3UWriter.kt | 11 +- .../retromusic/helper/MusicPlayerRemote.kt | 17 +- .../helper/MusicProgressViewUpdateHelper.kt | 10 +- .../helper/PlayPauseButtonOnClickHandler.kt | 11 +- .../retromusic/helper/SearchQueryHelper.kt | 12 +- .../monkey/retromusic/helper/ShuffleHelper.kt | 11 +- .../monkey/retromusic/helper/SortOrder.kt | 19 +- .../monkey/retromusic/helper/StackBlur.java | 600 ++-- .../monkey/retromusic/helper/StopWatch.kt | 10 +- .../retromusic/helper/menu/GenreMenuHelper.kt | 10 +- .../helper/menu/PlaylistMenuHelper.kt | 15 +- .../retromusic/helper/menu/SongMenuHelper.kt | 10 +- .../retromusic/helper/menu/SongsMenuHelper.kt | 10 +- .../interfaces/IAlbumClickListener.kt | 16 +- .../interfaces/IArtistClickListener.kt | 16 +- .../retromusic/interfaces/ICabHolder.kt | 11 +- .../retromusic/interfaces/ICallbacks.kt | 16 +- .../IMainActivityFragmentCallbacks.kt | 11 +- .../interfaces/IMusicServiceEventListener.kt | 11 +- .../interfaces/IPaletteColorHolder.kt | 10 +- .../name/monkey/retromusic/lyrics/Lrc.java | 38 +- .../monkey/retromusic/lyrics/LrcEntry.java | 163 +- .../monkey/retromusic/lyrics/LrcHelper.java | 198 +- .../monkey/retromusic/lyrics/LrcUtils.java | 331 ++- .../monkey/retromusic/lyrics/LrcView.java | 1337 ++++----- .../misc/CustomFragmentStatePagerAdapter.java | 383 +-- .../retromusic/misc/DialogAsyncTask.java | 136 +- .../retromusic/misc/GenericFileProvider.java | 3 +- .../monkey/retromusic/misc/LagTracker.java | 95 +- ...teToastMediaScannerCompletionListener.java | 76 +- .../model/lyrics/AbsSynchronizedLyrics.java | 71 +- .../retromusic/model/lyrics/Lyrics.java | 104 +- .../model/lyrics/SynchronizedLyricsLRC.java | 125 +- .../retromusic/network/model/LastFmAlbum.java | 272 +- .../network/model/LastFmArtist.java | 183 +- .../retromusic/network/model/LastFmTrack.java | 290 +- .../retromusic/providers/BlacklistStore.java | 290 +- .../retromusic/providers/HistoryStore.java | 269 +- .../providers/MusicPlaybackQueueStore.java | 337 ++- .../providers/SongPlayCountStore.java | 689 ++--- .../retromusic/repository/SortedCursor.java | 252 +- .../repository/SortedLongCursor.java | 249 +- .../retromusic/service/MultiPlayer.java | 579 ++-- .../retromusic/service/MusicService.java | 2459 +++++++++-------- .../retromusic/service/PlaybackHandler.java | 273 +- .../retromusic/util/ArtistSignatureUtil.java | 50 +- .../util/AutoGeneratedPlaylistBitmap.java | 313 ++- .../monkey/retromusic/util/BitmapEditor.java | 1813 ++++++------ .../monkey/retromusic/util/CalendarUtil.java | 214 +- .../monkey/retromusic/util/ColorUtil.java | 85 +- .../monkey/retromusic/util/Compressor.java | 89 +- .../name/monkey/retromusic/util/FileUtil.java | 393 ++- .../monkey/retromusic/util/ImageUtil.java | 466 ++-- .../monkey/retromusic/util/LyricUtil.java | 177 +- .../retromusic/util/NavigationUtil.java | 133 +- .../monkey/retromusic/util/PlaylistsUtil.java | 527 ++-- .../retromusic/util/RetroColorUtil.java | 345 +-- .../monkey/retromusic/util/RetroUtil.java | 281 +- .../monkey/retromusic/util/RippleUtils.java | 218 +- .../name/monkey/retromusic/util/SAFUtil.java | 479 ++-- .../retromusic/util/SwipeAndDragHelper.java | 90 +- .../monkey/retromusic/util/TempUtils.java | 114 +- .../retromusic/util/color/ImageUtils.java | 211 +- .../color/MediaNotificationProcessor.java | 849 +++--- .../util/color/NotificationColorUtil.java | 1832 ++++++------ .../views/BaselineGridTextView.java | 299 +- .../retromusic/views/BreadCrumbLayout.java | 721 ++--- .../retromusic/views/CircularImageView.java | 548 ++-- .../retromusic/views/ContributorsView.java | 91 +- .../retromusic/views/DrawableGradient.java | 27 +- .../views/HeightFitSquareLayout.java | 46 +- .../views/LollipopFixedWebView.java | 42 +- .../retromusic/views/NetworkImageView.java | 67 +- .../retromusic/views/PopupBackground.java | 227 +- ...ollingViewOnApplyWindowInsetsListener.java | 60 +- .../name/monkey/retromusic/views/SeekArc.java | 953 +++---- .../views/StatusBarMarginFrameLayout.java | 41 +- .../retromusic/views/StatusBarView.java | 58 +- .../retromusic/views/VerticalTextView.java | 74 +- .../volume/AudioVolumeContentObserver.java | 65 +- app/src/main/res/navigation/main_graph.xml | 4 +- build.gradle | 6 +- gradle.properties | 2 +- spotless.gradle | 30 + spotless.license.kt | 14 + 286 files changed, 15604 insertions(+), 13757 deletions(-) create mode 100644 spotless.gradle create mode 100644 spotless.license.kt diff --git a/app/build.gradle b/app/build.gradle index 462a02d14..c1eb4656a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,4 +166,6 @@ dependencies { implementation 'me.jorgecastillo:androidcolorx:0.2.0' implementation 'org.jsoup:jsoup:1.11.1' debugImplementation 'com.amitshekhar.android:debug-db:1.0.6' -} \ No newline at end of file +} + +apply from: '../spotless.gradle' \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/App.kt b/app/src/main/java/code/name/monkey/retromusic/App.kt index de9167932..4de597d3c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/App.kt +++ b/app/src/main/java/code/name/monkey/retromusic/App.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic import android.widget.Toast diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 55023db2a..452b2fa3f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic import android.provider.BaseColumns @@ -47,9 +47,9 @@ object Constants { MediaStore.Audio.AudioColumns.ALBUM_ID, // 7 MediaStore.Audio.AudioColumns.ALBUM, // 8 MediaStore.Audio.AudioColumns.ARTIST_ID, // 9 - MediaStore.Audio.AudioColumns.ARTIST,// 10 - MediaStore.Audio.AudioColumns.COMPOSER,// 11 - "album_artist"//12 + MediaStore.Audio.AudioColumns.ARTIST, // 10 + MediaStore.Audio.AudioColumns.COMPOSER, // 11 + "album_artist" // 12 ) const val NUMBER_OF_TOP_TRACKS = 99 } @@ -135,4 +135,4 @@ const val TOGGLE_SHUFFLE = "toggle_shuffle" const val SONG_GRID_STYLE = "song_grid_style" const val PAUSE_ON_ZERO_VOLUME = "pause_on_zero_volume" const val FILTER_SONG = "filter_song" -const val EXPAND_NOW_PLAYING_PANEL = "expand_now_playing_panel" \ No newline at end of file +const val EXPAND_NOW_PLAYING_PANEL = "expand_now_playing_panel" diff --git a/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt index f53120543..4f1273a03 100644 --- a/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt +++ b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic import androidx.annotation.IntDef @@ -25,4 +39,4 @@ const val GENRES = 6 const val PLAYLISTS = 7 const val HISTORY_PLAYLIST = 8 const val LAST_ADDED_PLAYLIST = 9 -const val TOP_PLAYED_PLAYLIST = 10 \ No newline at end of file +const val TOP_PLAYED_PLAYLIST = 10 diff --git a/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java b/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java index 36f7f5654..cbd9204e5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java +++ b/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java @@ -5,39 +5,37 @@ import android.content.ContextWrapper; import android.content.res.Configuration; import android.content.res.Resources; import android.os.LocaleList; - -import java.util.Locale; - import code.name.monkey.appthemehelper.util.VersionUtils; +import java.util.Locale; public class LanguageContextWrapper extends ContextWrapper { - public LanguageContextWrapper(Context base) { - super(base); + public LanguageContextWrapper(Context base) { + super(base); + } + + public static LanguageContextWrapper wrap(Context context, Locale newLocale) { + Resources res = context.getResources(); + Configuration configuration = res.getConfiguration(); + + if (VersionUtils.INSTANCE.hasNougatMR()) { + configuration.setLocale(newLocale); + + LocaleList localeList = new LocaleList(newLocale); + LocaleList.setDefault(localeList); + configuration.setLocales(localeList); + + context = context.createConfigurationContext(configuration); + + } else if (VersionUtils.INSTANCE.hasLollipop()) { + configuration.setLocale(newLocale); + context = context.createConfigurationContext(configuration); + + } else { + configuration.locale = newLocale; + res.updateConfiguration(configuration, res.getDisplayMetrics()); } - public static LanguageContextWrapper wrap(Context context, Locale newLocale) { - Resources res = context.getResources(); - Configuration configuration = res.getConfiguration(); - - if (VersionUtils.INSTANCE.hasNougatMR()) { - configuration.setLocale(newLocale); - - LocaleList localeList = new LocaleList(newLocale); - LocaleList.setDefault(localeList); - configuration.setLocales(localeList); - - context = context.createConfigurationContext(configuration); - - } else if (VersionUtils.INSTANCE.hasLollipop()) { - configuration.setLocale(newLocale); - context = context.createConfigurationContext(configuration); - - } else { - configuration.locale = newLocale; - res.updateConfiguration(configuration, res.getDisplayMetrics()); - } - - return new LanguageContextWrapper(context); - } -} \ No newline at end of file + return new LanguageContextWrapper(context); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java b/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java index abff707a4..345cda937 100644 --- a/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java +++ b/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java @@ -4,36 +4,32 @@ import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; - import androidx.coordinatorlayout.widget.CoordinatorLayout; - import com.google.android.material.bottomsheet.BottomSheetBehavior; - import org.jetbrains.annotations.NotNull; - public class RetroBottomSheetBehavior extends BottomSheetBehavior { - private static final String TAG = "RetroBottomSheetBehavior"; + private static final String TAG = "RetroBottomSheetBehavior"; - private boolean allowDragging = true; + private boolean allowDragging = true; - public RetroBottomSheetBehavior() { + public RetroBottomSheetBehavior() {} + + public RetroBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setAllowDragging(boolean allowDragging) { + this.allowDragging = allowDragging; + } + + @Override + public boolean onInterceptTouchEvent( + @NotNull CoordinatorLayout parent, @NotNull V child, @NotNull MotionEvent event) { + if (!allowDragging) { + return false; } - - public RetroBottomSheetBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setAllowDragging(boolean allowDragging) { - this.allowDragging = allowDragging; - } - - @Override - public boolean onInterceptTouchEvent(@NotNull CoordinatorLayout parent, @NotNull V child, @NotNull MotionEvent event) { - if (!allowDragging) { - return false; - } - return super.onInterceptTouchEvent(parent, child, event); - } -} \ No newline at end of file + return super.onInterceptTouchEvent(parent, child, event); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt index 13b75956d..3f5e7fbc0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2020 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.activities import android.animation.ObjectAnimator @@ -234,4 +234,4 @@ class DriveModeActivity : AbsMusicServiceActivity(), Callback { songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong()) songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong()) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java index eea9299cc..e66b7f812 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java @@ -18,82 +18,86 @@ import android.graphics.Color; import android.os.Bundle; import android.view.MenuItem; import android.webkit.WebView; - import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; - -import org.jetbrains.annotations.Nullable; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ColorUtil; import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.base.AbsBaseActivity; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import org.jetbrains.annotations.Nullable; -/** - * Created by hemanths on 2019-09-27. - */ +/** Created by hemanths on 2019-09-27. */ public class LicenseActivity extends AbsBaseActivity { - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - setDrawUnderStatusBar(); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_license); - setStatusbarColorAuto(); - setNavigationbarColorAuto(); - setLightNavigationBar(true); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ToolbarContentTintHelper.colorBackButton(toolbar); - toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); - WebView webView = findViewById(R.id.license); - try { - StringBuilder buf = new StringBuilder(); - InputStream json = getAssets().open("oldindex.html"); - BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); - String str; - while ((str = in.readLine()) != null) { - buf.append(str); - } - in.close(); + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + setDrawUnderStatusBar(); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_license); + setStatusbarColorAuto(); + setNavigationbarColorAuto(); + setLightNavigationBar(true); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ToolbarContentTintHelper.colorBackButton(toolbar); + toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); + WebView webView = findViewById(R.id.license); + try { + StringBuilder buf = new StringBuilder(); + InputStream json = getAssets().open("oldindex.html"); + BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); + String str; + while ((str = in.readLine()) != null) { + buf.append(str); + } + in.close(); - // Inject color values for WebView body background and links - final boolean isDark = ATHUtil.INSTANCE.isWindowBackgroundDark(this); - final String backgroundColor = colorToCSS(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface, - Color.parseColor(isDark ? "#424242" : "#ffffff"))); - final String contentColor = colorToCSS(Color.parseColor(isDark ? "#ffffff" : "#000000")); - final String changeLog = buf.toString() - .replace("{style-placeholder}", - String.format("body { background-color: %s; color: %s; }", backgroundColor, contentColor)) - .replace("{link-color}", colorToCSS(ThemeStore.Companion.accentColor(this))) - .replace("{link-color-active}", - colorToCSS(ColorUtil.INSTANCE.lightenColor(ThemeStore.Companion.accentColor(this)))); - - webView.loadData(changeLog, "text/html", "UTF-8"); - } catch (Throwable e) { - webView.loadData("

Unable to load

" + e.getLocalizedMessage() + "

", "text/html", "UTF-8"); - } + // Inject color values for WebView body background and links + final boolean isDark = ATHUtil.INSTANCE.isWindowBackgroundDark(this); + final String backgroundColor = + colorToCSS( + ATHUtil.INSTANCE.resolveColor( + this, R.attr.colorSurface, Color.parseColor(isDark ? "#424242" : "#ffffff"))); + final String contentColor = colorToCSS(Color.parseColor(isDark ? "#ffffff" : "#000000")); + final String changeLog = + buf.toString() + .replace( + "{style-placeholder}", + String.format( + "body { background-color: %s; color: %s; }", backgroundColor, contentColor)) + .replace("{link-color}", colorToCSS(ThemeStore.Companion.accentColor(this))) + .replace( + "{link-color-active}", + colorToCSS( + ColorUtil.INSTANCE.lightenColor(ThemeStore.Companion.accentColor(this)))); + webView.loadData(changeLog, "text/html", "UTF-8"); + } catch (Throwable e) { + webView.loadData( + "

Unable to load

" + e.getLocalizedMessage() + "

", "text/html", "UTF-8"); } + } - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); + } - private String colorToCSS(int color) { - return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), - Color.blue(color)); // on API 29, WebView doesn't load with hex colors - } + private String colorToCSS(int color) { + return String.format( + "rgb(%d, %d, %d)", + Color.red(color), + Color.green(color), + Color.blue(color)); // on API 29, WebView doesn't load with hex colors + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt index 9ff984373..ad3fa148f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.app.KeyguardManager @@ -101,4 +115,4 @@ class LockScreenActivity : AbsMusicServiceActivity() { } }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt index a40868424..25300b92a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.os.Bundle @@ -23,7 +37,6 @@ import com.google.android.material.transition.platform.MaterialContainerTransfor import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import kotlinx.android.synthetic.main.activity_lyrics.* - class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper.Callback { private lateinit var updateHelper: MusicProgressViewUpdateHelper @@ -38,7 +51,7 @@ class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper. return baseUrl } - private fun buildContainerTransform( ): MaterialContainerTransform { + private fun buildContainerTransform(): MaterialContainerTransform { val transform = MaterialContainerTransform() transform.setAllContainerColors( MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface) @@ -53,8 +66,8 @@ class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper. override fun onCreate(savedInstanceState: Bundle?) { findViewById(android.R.id.content).transitionName = "lyrics" setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) - window.sharedElementEnterTransition = buildContainerTransform( ) - window.sharedElementReturnTransition = buildContainerTransform( ) + window.sharedElementEnterTransition = buildContainerTransform() + window.sharedElementReturnTransition = buildContainerTransform() super.onCreate(savedInstanceState) setContentView(R.layout.activity_lyrics) setStatusbarColorAuto() @@ -145,4 +158,4 @@ class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper. } return super.onOptionsItemSelected(item) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index a9d6bdbbd..eeb6426e7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.content.Intent @@ -47,7 +61,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis AppRater.appLaunched(this) updateTabs() - //NavigationUI.setupWithNavController(getBottomNavigationView(), findNavController(R.id.fragment_container)) + // NavigationUI.setupWithNavController(getBottomNavigationView(), findNavController(R.id.fragment_container)) setupNavigationController() if (!hasPermissions()) { findNavController(R.id.fragment_container).navigate(R.id.permissionFragment) @@ -66,7 +80,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis navController.graph = navGraph NavigationUI.setupWithNavController(getBottomNavigationView(), navController) navController.addOnDestinationChangedListener { _, _, _ -> - //appBarLayout.setExpanded(true, true) + // appBarLayout.setExpanded(true, true) } } @@ -156,11 +170,11 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis setIntent(Intent()) } } - } private fun parseLongFromIntent( - intent: Intent, longKey: String, + intent: Intent, + longKey: String, stringKey: String ): Long { var id = intent.getLongExtra(longKey, -1) @@ -176,4 +190,4 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis } return id } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt index de64f8f0a..543ca1c3e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.content.Intent @@ -15,7 +29,6 @@ import code.name.monkey.retromusic.util.RingtoneManager import kotlinx.android.synthetic.main.activity_permission.* import kotlinx.android.synthetic.main.fragment_library.appNameText - class PermissionActivity : AbsMusicServiceActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,7 +52,7 @@ class PermissionActivity : AbsMusicServiceActivity() { } finish.accentBackgroundColor() finish.setOnClickListener { - if (hasPermissions() ) { + if (hasPermissions()) { startActivity( Intent(this, MainActivity::class.java).addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or @@ -60,4 +73,4 @@ class PermissionActivity : AbsMusicServiceActivity() { ) appNameText.text = appName } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt index b02622f86..e8a24424c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PlayingQueueActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.content.res.ColorStateList @@ -185,4 +199,4 @@ open class PlayingQueueActivity : AbsMusicServiceActivity() { clearQueue.iconTint = this } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt index 99d30ab70..573ab2530 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.content.Intent @@ -17,8 +31,8 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsBaseActivity import com.anjlab.android.iab.v3.BillingProcessor import com.anjlab.android.iab.v3.TransactionDetails -import kotlinx.android.synthetic.main.activity_pro_version.* import java.lang.ref.WeakReference +import kotlinx.android.synthetic.main.activity_pro_version.* class PurchaseActivity : AbsBaseActivity(), BillingProcessor.IBillingHandler { @@ -47,7 +61,6 @@ class PurchaseActivity : AbsBaseActivity(), BillingProcessor.IBillingHandler { if (restorePurchaseAsyncTask == null || restorePurchaseAsyncTask!!.status != AsyncTask.Status.RUNNING) { restorePurchase() } - } purchaseButton.setOnClickListener { billingProcessor.purchase(this@PurchaseActivity, PRO_VERSION_PRODUCT_ID) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt index 2e1384528..5fdada745 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.os.Bundle @@ -49,7 +63,6 @@ class SettingsActivity : AbsBaseActivity(), ColorChooserDialog.ColorCallback { } override fun onColorChooserDismissed(dialog: ColorChooserDialog) { - } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -58,4 +71,4 @@ class SettingsActivity : AbsBaseActivity(), ColorChooserDialog.ColorCallback { } return super.onOptionsItemSelected(item) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt b/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt index 3b3f36049..1bcd169cf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2020 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.activities import android.content.res.ColorStateList diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt index b20d70458..7efc576a6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/SupportDevelopmentActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.content.Intent @@ -28,9 +42,9 @@ import code.name.monkey.retromusic.extensions.textColorSecondary import com.anjlab.android.iab.v3.BillingProcessor import com.anjlab.android.iab.v3.SkuDetails import com.anjlab.android.iab.v3.TransactionDetails -import kotlinx.android.synthetic.main.activity_donation.* import java.lang.ref.WeakReference import java.util.* +import kotlinx.android.synthetic.main.activity_donation.* class SupportDevelopmentActivity : AbsBaseActivity(), BillingProcessor.IBillingHandler { @@ -91,7 +105,7 @@ class SupportDevelopmentActivity : AbsBaseActivity(), BillingProcessor.IBillingH } override fun onProductPurchased(productId: String, details: TransactionDetails?) { - //loadSkuDetails(); + // loadSkuDetails(); Toast.makeText(this, R.string.thank_you, Toast.LENGTH_SHORT).show() } @@ -100,7 +114,7 @@ class SupportDevelopmentActivity : AbsBaseActivity(), BillingProcessor.IBillingH } override fun onPurchaseHistoryRestored() { - //loadSkuDetails(); + // loadSkuDetails(); Toast.makeText(this, R.string.restored_previous_purchases, Toast.LENGTH_SHORT).show() } @@ -110,7 +124,7 @@ class SupportDevelopmentActivity : AbsBaseActivity(), BillingProcessor.IBillingH } if (requestCode == TEZ_REQUEST_CODE) { // Process based on the data in response. - //Log.d("result", data!!.getStringExtra("Status")) + // Log.d("result", data!!.getStringExtra("Status")) } } @@ -165,7 +179,8 @@ private class SkuDetailsLoadAsyncTask(supportDevelopmentActivity: SupportDevelop } class SkuDetailsAdapter( - private var donationsDialog: SupportDevelopmentActivity, objects: List + private var donationsDialog: SupportDevelopmentActivity, + objects: List ) : RecyclerView.Adapter() { private var skuDetailsList: List = ArrayList() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt index eda1e1b03..a3bf682ad 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/UserInfoActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities import android.app.Activity @@ -27,15 +41,15 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.constant.ImageProvider +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import kotlinx.android.synthetic.main.activity_user_info.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.IOException class UserInfoActivity : AbsBaseActivity() { @@ -114,7 +128,6 @@ class UserInfoActivity : AbsBaseActivity() { .start(PICK_IMAGE_REQUEST) } - public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && requestCode == PICK_IMAGE_REQUEST) { @@ -209,9 +222,8 @@ class UserInfoActivity : AbsBaseActivity() { .into(userImage) } - companion object { private const val PICK_IMAGE_REQUEST = 9002 private const val PICK_BANNER_REQUEST = 9004 } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewActivity.java b/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewActivity.java index 4f1a6ef3f..00d16e9a1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewActivity.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewActivity.java @@ -1,23 +1,14 @@ package code.name.monkey.retromusic.activities; - import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Bundle; import android.webkit.WebView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Locale; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ColorUtil; @@ -26,65 +17,95 @@ import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.base.AbsBaseActivity; import code.name.monkey.retromusic.util.PreferenceUtil; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Locale; public class WhatsNewActivity extends AbsBaseActivity { - private static String colorToCSS(int color) { - return String.format(Locale.getDefault(), "rgba(%d, %d, %d, %d)", Color.red(color), Color.green(color), - Color.blue(color), Color.alpha(color)); // on API 29, WebView doesn't load with hex colors + private static String colorToCSS(int color) { + return String.format( + Locale.getDefault(), + "rgba(%d, %d, %d, %d)", + Color.red(color), + Color.green(color), + Color.blue(color), + Color.alpha(color)); // on API 29, WebView doesn't load with hex colors + } + + private static void setChangelogRead(@NonNull Context context) { + try { + PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + int currentVersion = pInfo.versionCode; + PreferenceUtil.INSTANCE.setLastVersion(currentVersion); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); } + } - private static void setChangelogRead(@NonNull Context context) { - try { - PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - int currentVersion = pInfo.versionCode; - PreferenceUtil.INSTANCE.setLastVersion(currentVersion); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + setDrawUnderStatusBar(); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_whats_new); + setStatusbarColorAuto(); + setNavigationbarColorAuto(); + setTaskDescriptionColorAuto(); + + WebView webView = findViewById(R.id.webView); + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); + toolbar.setNavigationOnClickListener(v -> onBackPressed()); + ToolbarContentTintHelper.colorBackButton(toolbar); + + try { + StringBuilder buf = new StringBuilder(); + InputStream json = getAssets().open("retro-changelog.html"); + BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); + String str; + while ((str = in.readLine()) != null) { + buf.append(str); + } + in.close(); + + // Inject color values for WebView body background and links + final boolean isDark = ATHUtil.INSTANCE.isWindowBackgroundDark(this); + final int accentColor = ThemeStore.Companion.accentColor(this); + final String backgroundColor = + colorToCSS( + ATHUtil.INSTANCE.resolveColor( + this, R.attr.colorSurface, Color.parseColor(isDark ? "#424242" : "#ffffff"))); + final String contentColor = colorToCSS(Color.parseColor(isDark ? "#ffffff" : "#000000")); + final String textColor = colorToCSS(Color.parseColor(isDark ? "#60FFFFFF" : "#80000000")); + final String accentColorString = colorToCSS(ThemeStore.Companion.accentColor(this)); + final String accentTextColor = + colorToCSS( + MaterialValueHelper.getPrimaryTextColor( + this, ColorUtil.INSTANCE.isColorLight(accentColor))); + final String changeLog = + buf.toString() + .replace( + "{style-placeholder}", + String.format( + "body { background-color: %s; color: %s; } li {color: %s;} .colorHeader {background-color: %s; color: %s;} .tag {color: %s;}", + backgroundColor, + contentColor, + textColor, + accentColorString, + accentTextColor, + accentColorString)) + .replace("{link-color}", colorToCSS(ThemeStore.Companion.accentColor(this))) + .replace( + "{link-color-active}", + colorToCSS( + ColorUtil.INSTANCE.lightenColor(ThemeStore.Companion.accentColor(this)))); + webView.loadData(changeLog, "text/html", "UTF-8"); + } catch (Throwable e) { + webView.loadData( + "

Unable to load

" + e.getLocalizedMessage() + "

", "text/html", "UTF-8"); } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - setDrawUnderStatusBar(); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_whats_new); - setStatusbarColorAuto(); - setNavigationbarColorAuto(); - setTaskDescriptionColorAuto(); - - WebView webView = findViewById(R.id.webView); - Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); - ToolbarContentTintHelper.colorBackButton(toolbar); - - try { - StringBuilder buf = new StringBuilder(); - InputStream json = getAssets().open("retro-changelog.html"); - BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); - String str; - while ((str = in.readLine()) != null) { - buf.append(str); - } - in.close(); - - // Inject color values for WebView body background and links - final boolean isDark = ATHUtil.INSTANCE.isWindowBackgroundDark(this); - final int accentColor = ThemeStore.Companion.accentColor(this); - final String backgroundColor = colorToCSS(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface, Color.parseColor(isDark ? "#424242" : "#ffffff"))); - final String contentColor = colorToCSS(Color.parseColor(isDark ? "#ffffff" : "#000000")); - final String textColor = colorToCSS(Color.parseColor(isDark ? "#60FFFFFF" : "#80000000")); - final String accentColorString = colorToCSS(ThemeStore.Companion.accentColor(this)); - final String accentTextColor = colorToCSS(MaterialValueHelper.getPrimaryTextColor(this, ColorUtil.INSTANCE.isColorLight(accentColor))); - final String changeLog = buf.toString() - .replace("{style-placeholder}", String.format("body { background-color: %s; color: %s; } li {color: %s;} .colorHeader {background-color: %s; color: %s;} .tag {color: %s;}", backgroundColor, contentColor, textColor, accentColorString, accentTextColor, accentColorString)) - .replace("{link-color}", colorToCSS(ThemeStore.Companion.accentColor(this))) - .replace("{link-color-active}", colorToCSS(ColorUtil.INSTANCE.lightenColor(ThemeStore.Companion.accentColor(this)))); - webView.loadData(changeLog, "text/html", "UTF-8"); - } catch (Throwable e) { - webView.loadData("

Unable to load

" + e.getLocalizedMessage() + "

", "text/html", "UTF-8"); - } - setChangelogRead(this); - } -} \ No newline at end of file + setChangelogRead(this); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsBaseActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsBaseActivity.kt index 71192a725..acec7d627 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsBaseActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsBaseActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.base import android.Manifest @@ -46,7 +60,7 @@ abstract class AbsBaseActivity : AbsThemeActivity() { override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) if (!hasPermissions()) { - //requestPermissions() + // requestPermissions() } } @@ -107,7 +121,7 @@ abstract class AbsBaseActivity : AbsThemeActivity() { this@AbsBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE ) ) { - //User has deny from permission dialog + // User has deny from permission dialog Snackbar.make( snackBarContainer, permissionDeniedMessage!!, diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt index 7b13a4bca..cef132765 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt @@ -1,7 +1,26 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.base import android.Manifest -import android.content.* +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import androidx.lifecycle.lifecycleScope @@ -11,11 +30,11 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.repository.RealRepository import code.name.monkey.retromusic.service.MusicService.* +import java.lang.ref.WeakReference +import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject -import java.lang.ref.WeakReference -import java.util.* abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventListener { diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 277040f6d..009912afc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.base import android.annotation.SuppressLint @@ -148,7 +162,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { miniPlayerFragment?.view?.alpha = alpha miniPlayerFragment?.view?.visibility = if (alpha == 0f) View.GONE else View.VISIBLE bottomNavigationView.translationY = progress * 500 - //bottomNavigationView.alpha = alpha + // bottomNavigationView.alpha = alpha } open fun onPanelCollapsed() { @@ -177,7 +191,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { STATE_EXPANDED -> onPanelExpanded() STATE_COLLAPSED -> onPanelCollapsed() else -> { - //playerFragment!!.onHide() + // playerFragment!!.onHide() } } } @@ -398,4 +412,4 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { miniPlayerFragment = whichFragment(R.id.miniPlayerFragment) miniPlayerFragment?.view?.setOnClickListener { expandPanel() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt index 6746329b5..780f50def 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.base import android.content.Context @@ -37,7 +51,6 @@ abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable { MaterialDialogsUtil.updateMaterialDialogsThemeSingleton(this) } - private fun updateTheme() { setTheme(ThemeManager.getThemeResValue(this)) setDefaultNightMode(ThemeManager.getNightMode(this)) @@ -204,4 +217,4 @@ abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable { super.attachBaseContext(LanguageContextWrapper.wrap(newBase, Locale(code))) } else super.attachBaseContext(newBase) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/BugReportActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/BugReportActivity.kt index 5bb2b7aca..7c7eb81ed 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/BugReportActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/BugReportActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.bugreport import android.app.Activity @@ -31,6 +45,7 @@ import code.name.monkey.retromusic.misc.DialogAsyncTask import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.textfield.TextInputLayout +import java.io.IOException import kotlinx.android.synthetic.main.activity_bug_report.* import kotlinx.android.synthetic.main.bug_report_card_device_info.* import kotlinx.android.synthetic.main.bug_report_card_report.* @@ -38,7 +53,6 @@ import org.eclipse.egit.github.core.Issue import org.eclipse.egit.github.core.client.GitHubClient import org.eclipse.egit.github.core.client.RequestException import org.eclipse.egit.github.core.service.IssueService -import java.io.IOException private const val RESULT_SUCCESS = "RESULT_OK" private const val RESULT_BAD_CREDENTIALS = "RESULT_BAD_CREDENTIALS" @@ -306,7 +320,6 @@ open class BugReportActivity : AbsThemeActivity() { } } - companion object { fun report( diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.java b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.java index 3a6e80328..79e9b0a0c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.java @@ -5,126 +5,196 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; - import androidx.annotation.IntRange; - +import code.name.monkey.retromusic.util.PreferenceUtil; import java.util.Arrays; import java.util.Locale; -import code.name.monkey.retromusic.util.PreferenceUtil; - public class DeviceInfo { - @SuppressLint("NewApi") - @SuppressWarnings("deprecation") - private final String[] abis = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? - Build.SUPPORTED_ABIS : new String[]{Build.CPU_ABI, Build.CPU_ABI2}; + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + private final String[] abis = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + ? Build.SUPPORTED_ABIS + : new String[] {Build.CPU_ABI, Build.CPU_ABI2}; - @SuppressLint("NewApi") - private final String[] abis32Bits = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? - Build.SUPPORTED_32_BIT_ABIS : null; + @SuppressLint("NewApi") + private final String[] abis32Bits = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_32_BIT_ABIS : null; - @SuppressLint("NewApi") - private final String[] abis64Bits = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? - Build.SUPPORTED_64_BIT_ABIS : null; + @SuppressLint("NewApi") + private final String[] abis64Bits = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_64_BIT_ABIS : null; - private final String baseTheme; + private final String baseTheme; - private final String brand = Build.BRAND; + private final String brand = Build.BRAND; - private final String buildID = Build.DISPLAY; + private final String buildID = Build.DISPLAY; - private final String buildVersion = Build.VERSION.INCREMENTAL; + private final String buildVersion = Build.VERSION.INCREMENTAL; - private final String device = Build.DEVICE; + private final String device = Build.DEVICE; - private final String hardware = Build.HARDWARE; + private final String hardware = Build.HARDWARE; - private final boolean isAdaptive; + private final boolean isAdaptive; - private final String manufacturer = Build.MANUFACTURER; + private final String manufacturer = Build.MANUFACTURER; - private final String model = Build.MODEL; + private final String model = Build.MODEL; - private final String nowPlayingTheme; + private final String nowPlayingTheme; - private final String product = Build.PRODUCT; + private final String product = Build.PRODUCT; - private final String releaseVersion = Build.VERSION.RELEASE; + private final String releaseVersion = Build.VERSION.RELEASE; - @IntRange(from = 0) - private final int sdkVersion = Build.VERSION.SDK_INT; + @IntRange(from = 0) + private final int sdkVersion = Build.VERSION.SDK_INT; - private final int versionCode; + private final int versionCode; - private final String versionName; - private final String selectedLang; + private final String versionName; + private final String selectedLang; - public DeviceInfo(Context context) { - PackageInfo packageInfo; - try { - packageInfo = context.getPackageManager() - .getPackageInfo(context.getPackageName(), 0); - } catch (PackageManager.NameNotFoundException e) { - packageInfo = null; - } - if (packageInfo != null) { - versionCode = packageInfo.versionCode; - versionName = packageInfo.versionName; - } else { - versionCode = -1; - versionName = null; - } - baseTheme = PreferenceUtil.INSTANCE.getBaseTheme(); - nowPlayingTheme = context.getString(PreferenceUtil.INSTANCE.getNowPlayingScreen().getTitleRes()); - isAdaptive = PreferenceUtil.INSTANCE.isAdaptiveColor(); - selectedLang = PreferenceUtil.INSTANCE.getLanguageCode(); + public DeviceInfo(Context context) { + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + packageInfo = null; } - - public String toMarkdown() { - return "Device info:\n" - + "---\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "
App version" + versionName + "
App version code" + versionCode + "
Android build version" + buildVersion + "
Android release version" + releaseVersion + "
Android SDK version" + sdkVersion + "
Android build ID" + buildID + "
Device brand" + brand + "
Device manufacturer" + manufacturer + "
Device name" + device + "
Device model" + model + "
Device product name" + product + "
Device hardware name" + hardware + "
ABIs" + Arrays.toString(abis) + "
ABIs (32bit)" + Arrays.toString(abis32Bits) + "
ABIs (64bit)" + Arrays.toString(abis64Bits) + "
Language" + selectedLang + "
\n"; + if (packageInfo != null) { + versionCode = packageInfo.versionCode; + versionName = packageInfo.versionName; + } else { + versionCode = -1; + versionName = null; } + baseTheme = PreferenceUtil.INSTANCE.getBaseTheme(); + nowPlayingTheme = + context.getString(PreferenceUtil.INSTANCE.getNowPlayingScreen().getTitleRes()); + isAdaptive = PreferenceUtil.INSTANCE.isAdaptiveColor(); + selectedLang = PreferenceUtil.INSTANCE.getLanguageCode(); + } - @Override - public String toString() { - return "App version: " + versionName + "\n" - + "App version code: " + versionCode + "\n" - + "Android build version: " + buildVersion + "\n" - + "Android release version: " + releaseVersion + "\n" - + "Android SDK version: " + sdkVersion + "\n" - + "Android build ID: " + buildID + "\n" - + "Device brand: " + brand + "\n" - + "Device manufacturer: " + manufacturer + "\n" - + "Device name: " + device + "\n" - + "Device model: " + model + "\n" - + "Device product name: " + product + "\n" - + "Device hardware name: " + hardware + "\n" - + "ABIs: " + Arrays.toString(abis) + "\n" - + "ABIs (32bit): " + Arrays.toString(abis32Bits) + "\n" - + "ABIs (64bit): " + Arrays.toString(abis64Bits) + "\n" - + "Base theme: " + baseTheme + "\n" - + "Now playing theme: " + nowPlayingTheme + "\n" - + "Adaptive: " + isAdaptive + "\n" - + "System language: " + Locale.getDefault().toLanguageTag() + "\n" - + "In-App Language: " + selectedLang; - } + public String toMarkdown() { + return "Device info:\n" + + "---\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
App version" + + versionName + + "
App version code" + + versionCode + + "
Android build version" + + buildVersion + + "
Android release version" + + releaseVersion + + "
Android SDK version" + + sdkVersion + + "
Android build ID" + + buildID + + "
Device brand" + + brand + + "
Device manufacturer" + + manufacturer + + "
Device name" + + device + + "
Device model" + + model + + "
Device product name" + + product + + "
Device hardware name" + + hardware + + "
ABIs" + + Arrays.toString(abis) + + "
ABIs (32bit)" + + Arrays.toString(abis32Bits) + + "
ABIs (64bit)" + + Arrays.toString(abis64Bits) + + "
Language" + + selectedLang + + "
\n"; + } + + @Override + public String toString() { + return "App version: " + + versionName + + "\n" + + "App version code: " + + versionCode + + "\n" + + "Android build version: " + + buildVersion + + "\n" + + "Android release version: " + + releaseVersion + + "\n" + + "Android SDK version: " + + sdkVersion + + "\n" + + "Android build ID: " + + buildID + + "\n" + + "Device brand: " + + brand + + "\n" + + "Device manufacturer: " + + manufacturer + + "\n" + + "Device name: " + + device + + "\n" + + "Device model: " + + model + + "\n" + + "Device product name: " + + product + + "\n" + + "Device hardware name: " + + hardware + + "\n" + + "ABIs: " + + Arrays.toString(abis) + + "\n" + + "ABIs (32bit): " + + Arrays.toString(abis32Bits) + + "\n" + + "ABIs (64bit): " + + Arrays.toString(abis64Bits) + + "\n" + + "Base theme: " + + baseTheme + + "\n" + + "Now playing theme: " + + nowPlayingTheme + + "\n" + + "Adaptive: " + + isAdaptive + + "\n" + + "System language: " + + Locale.getDefault().toLanguageTag() + + "\n" + + "In-App Language: " + + selectedLang; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/Report.java b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/Report.java index 1da9313bb..ee1910c2c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/Report.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/Report.java @@ -1,33 +1,34 @@ package code.name.monkey.retromusic.activities.bugreport.model; - import code.name.monkey.retromusic.activities.bugreport.model.github.ExtraInfo; public class Report { - private final String description; + private final String description; - private final DeviceInfo deviceInfo; + private final DeviceInfo deviceInfo; - private final ExtraInfo extraInfo; + private final ExtraInfo extraInfo; - private final String title; + private final String title; - public Report(String title, String description, DeviceInfo deviceInfo, ExtraInfo extraInfo) { - this.title = title; - this.description = description; - this.deviceInfo = deviceInfo; - this.extraInfo = extraInfo; - } + public Report(String title, String description, DeviceInfo deviceInfo, ExtraInfo extraInfo) { + this.title = title; + this.description = description; + this.deviceInfo = deviceInfo; + this.extraInfo = extraInfo; + } - public String getDescription() { - return description + "\n\n" - + "-\n\n" - + deviceInfo.toMarkdown() + "\n\n" - + extraInfo.toMarkdown(); - } + public String getDescription() { + return description + + "\n\n" + + "-\n\n" + + deviceInfo.toMarkdown() + + "\n\n" + + extraInfo.toMarkdown(); + } - public String getTitle() { - return title; - } + public String getTitle() { + return title; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/ExtraInfo.java b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/ExtraInfo.java index ec27388cb..4bc0e4bfb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/ExtraInfo.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/ExtraInfo.java @@ -5,58 +5,57 @@ import java.util.Map; public class ExtraInfo { - private final Map extraInfo = new LinkedHashMap<>(); + private final Map extraInfo = new LinkedHashMap<>(); - public void put(String key, String value) { - extraInfo.put(key, value); + public void put(String key, String value) { + extraInfo.put(key, value); + } + + public void put(String key, boolean value) { + extraInfo.put(key, Boolean.toString(value)); + } + + public void put(String key, double value) { + extraInfo.put(key, Double.toString(value)); + } + + public void put(String key, float value) { + extraInfo.put(key, Float.toString(value)); + } + + public void put(String key, long value) { + extraInfo.put(key, Long.toString(value)); + } + + public void put(String key, int value) { + extraInfo.put(key, Integer.toString(value)); + } + + public void put(String key, Object value) { + extraInfo.put(key, String.valueOf(value)); + } + + public void remove(String key) { + extraInfo.remove(key); + } + + public String toMarkdown() { + if (extraInfo.isEmpty()) { + return ""; } - public void put(String key, boolean value) { - extraInfo.put(key, Boolean.toString(value)); + StringBuilder output = new StringBuilder(); + output.append("Extra info:\n" + "---\n" + "\n"); + for (String key : extraInfo.keySet()) { + output + .append("\n"); } + output.append("
") + .append(key) + .append("") + .append(extraInfo.get(key)) + .append("
\n"); - public void put(String key, double value) { - extraInfo.put(key, Double.toString(value)); - } - - public void put(String key, float value) { - extraInfo.put(key, Float.toString(value)); - } - - public void put(String key, long value) { - extraInfo.put(key, Long.toString(value)); - } - - public void put(String key, int value) { - extraInfo.put(key, Integer.toString(value)); - } - - public void put(String key, Object value) { - extraInfo.put(key, String.valueOf(value)); - } - - public void remove(String key) { - extraInfo.remove(key); - } - - public String toMarkdown() { - if (extraInfo.isEmpty()) { - return ""; - } - - StringBuilder output = new StringBuilder(); - output.append("Extra info:\n" - + "---\n" - + "\n"); - for (String key : extraInfo.keySet()) { - output.append("\n"); - } - output.append("
") - .append(key) - .append("") - .append(extraInfo.get(key)) - .append("
\n"); - - return output.toString(); - } + return output.toString(); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubLogin.java b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubLogin.java index e388249cb..71a0ce7b9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubLogin.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubLogin.java @@ -4,38 +4,37 @@ import android.text.TextUtils; public class GithubLogin { - private final String apiToken; + private final String apiToken; - private final String password; + private final String password; - private final String username; + private final String username; - public GithubLogin(String username, String password) { - this.username = username; - this.password = password; - this.apiToken = null; - } + public GithubLogin(String username, String password) { + this.username = username; + this.password = password; + this.apiToken = null; + } - public GithubLogin(String apiToken) { - this.username = null; - this.password = null; - this.apiToken = apiToken; - } + public GithubLogin(String apiToken) { + this.username = null; + this.password = null; + this.apiToken = apiToken; + } - public String getApiToken() { - return apiToken; - } + public String getApiToken() { + return apiToken; + } - public String getPassword() { - return password; - } + public String getPassword() { + return password; + } - public String getUsername() { - return username; - } - - public boolean shouldUseApiToken() { - return TextUtils.isEmpty(username) || TextUtils.isEmpty(password); - } + public String getUsername() { + return username; + } + public boolean shouldUseApiToken() { + return TextUtils.isEmpty(username) || TextUtils.isEmpty(password); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubTarget.java b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubTarget.java index 21126d30c..9e533bc7c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubTarget.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/github/GithubTarget.java @@ -2,20 +2,20 @@ package code.name.monkey.retromusic.activities.bugreport.model.github; public class GithubTarget { - private final String repository; + private final String repository; - private final String username; + private final String username; - public GithubTarget(String username, String repository) { - this.username = username; - this.repository = repository; - } + public GithubTarget(String username, String repository) { + this.username = username; + this.repository = repository; + } - public String getRepository() { - return repository; - } + public String getRepository() { + return repository; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java index a5f2d9083..46fdd786f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java @@ -16,57 +16,58 @@ package code.name.monkey.retromusic.activities.saf; import android.os.Build; import android.os.Bundle; - import androidx.annotation.Nullable; - +import code.name.monkey.retromusic.R; import com.heinrichreimersoftware.materialintro.app.IntroActivity; import com.heinrichreimersoftware.materialintro.slide.SimpleSlide; -import code.name.monkey.retromusic.R; - -/** - * Created by hemanths on 2019-07-31. - */ +/** Created by hemanths on 2019-07-31. */ public class SAFGuideActivity extends IntroActivity { - public static final int REQUEST_CODE_SAF_GUIDE = 98; + public static final int REQUEST_CODE_SAF_GUIDE = 98; - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - setButtonCtaVisible(false); - setButtonNextVisible(false); - setButtonBackVisible(false); + setButtonCtaVisible(false); + setButtonNextVisible(false); + setButtonBackVisible(false); - setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT); + setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT); - String title = String.format(getString(R.string.saf_guide_slide1_title), getString(R.string.app_name)); + String title = + String.format(getString(R.string.saf_guide_slide1_title), getString(R.string.app_name)); - addSlide(new SimpleSlide.Builder() - .title(title) - .description(Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 - ? R.string.saf_guide_slide1_description_before_o : R.string.saf_guide_slide1_description) - .image(R.drawable.saf_guide_1) - .background(R.color.md_deep_purple_300) - .backgroundDark(R.color.md_deep_purple_400) - .layout(R.layout.fragment_simple_slide_large_image) - .build()); - addSlide(new SimpleSlide.Builder() - .title(R.string.saf_guide_slide2_title) - .description(R.string.saf_guide_slide2_description) - .image(R.drawable.saf_guide_2) - .background(R.color.md_deep_purple_500) - .backgroundDark(R.color.md_deep_purple_600) - .layout(R.layout.fragment_simple_slide_large_image) - .build()); - addSlide(new SimpleSlide.Builder() - .title(R.string.saf_guide_slide3_title) - .description(R.string.saf_guide_slide3_description) - .image(R.drawable.saf_guide_3) - .background(R.color.md_deep_purple_700) - .backgroundDark(R.color.md_deep_purple_800) - .layout(R.layout.fragment_simple_slide_large_image) - .build()); - } + addSlide( + new SimpleSlide.Builder() + .title(title) + .description( + Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1 + ? R.string.saf_guide_slide1_description_before_o + : R.string.saf_guide_slide1_description) + .image(R.drawable.saf_guide_1) + .background(R.color.md_deep_purple_300) + .backgroundDark(R.color.md_deep_purple_400) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + addSlide( + new SimpleSlide.Builder() + .title(R.string.saf_guide_slide2_title) + .description(R.string.saf_guide_slide2_description) + .image(R.drawable.saf_guide_2) + .background(R.color.md_deep_purple_500) + .backgroundDark(R.color.md_deep_purple_600) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + addSlide( + new SimpleSlide.Builder() + .title(R.string.saf_guide_slide3_title) + .description(R.string.saf_guide_slide3_description) + .image(R.drawable.saf_guide_3) + .background(R.color.md_deep_purple_700) + .backgroundDark(R.color.md_deep_purple_800) + .layout(R.layout.fragment_simple_slide_large_image) + .build()); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt index 7bb8a49c1..9f40db14d 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.tageditor import android.app.Activity @@ -28,13 +42,13 @@ import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.SAFUtil import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import java.util.* import kotlinx.android.synthetic.main.activity_album_tag_editor.* import org.jaudiotagger.audio.AudioFile import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.tag.FieldKey import org.koin.android.ext.android.inject -import java.io.File -import java.util.* abstract class AbsTagEditorActivity : AbsBaseActivity() { val repository by inject() @@ -324,7 +338,8 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { } protected fun writeValuesToFiles( - fieldKeyValueMap: Map, artworkInfo: ArtworkInfo? + fieldKeyValueMap: Map, + artworkInfo: ArtworkInfo? ) { RetroUtil.hideSoftKeyboard(this) @@ -405,5 +420,4 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { private val TAG = AbsTagEditorActivity::class.java.simpleName private const val REQUEST_CODE_SELECT_IMAGE = 1000 } - } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt index a54633fac..957413ffb 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.tageditor import android.app.Activity @@ -26,9 +40,9 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.animation.GlideAnimation import com.bumptech.glide.request.target.SimpleTarget +import java.util.* import kotlinx.android.synthetic.main.activity_album_tag_editor.* import org.jaudiotagger.tag.FieldKey -import java.util.* class AlbumTagEditorActivity : AbsTagEditorActivity(), TextWatcher { @@ -155,7 +169,7 @@ class AlbumTagEditorActivity : AbsTagEditorActivity(), TextWatcher { override fun save() { val fieldKeyValueMap = EnumMap(FieldKey::class.java) fieldKeyValueMap[FieldKey.ALBUM] = albumText.text.toString() - //android seems not to recognize album_artist field so we additionally write the normal artist field + // android seems not to recognize album_artist field so we additionally write the normal artist field fieldKeyValueMap[FieldKey.ARTIST] = albumArtistText.text.toString() fieldKeyValueMap[FieldKey.ALBUM_ARTIST] = albumArtistText.text.toString() fieldKeyValueMap[FieldKey.GENRE] = genreTitle.text.toString() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt index 35b02bda3..4446bcfee 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.activities.tageditor import android.net.Uri @@ -9,10 +23,10 @@ import code.name.monkey.appthemehelper.util.MaterialUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.extensions.appHandleColor import code.name.monkey.retromusic.repository.SongRepository +import java.util.* import kotlinx.android.synthetic.main.activity_song_tag_editor.* import org.jaudiotagger.tag.FieldKey import org.koin.android.ext.android.inject -import java.util.* class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { @@ -111,5 +125,3 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { val TAG: String = SongTagEditorActivity::class.java.simpleName } } - - diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java index 650fc6cca..405819e60 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java @@ -7,19 +7,14 @@ import android.graphics.Bitmap; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.misc.DialogAsyncTask; +import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; +import code.name.monkey.retromusic.util.MusicUtil; +import code.name.monkey.retromusic.util.SAFUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.jaudiotagger.audio.AudioFile; -import org.jaudiotagger.audio.AudioFileIO; -import org.jaudiotagger.tag.FieldKey; -import org.jaudiotagger.tag.Tag; -import org.jaudiotagger.tag.images.Artwork; -import org.jaudiotagger.tag.images.ArtworkFactory; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -27,166 +22,171 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Map; +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.tag.FieldKey; +import org.jaudiotagger.tag.Tag; +import org.jaudiotagger.tag.images.Artwork; +import org.jaudiotagger.tag.images.ArtworkFactory; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.misc.DialogAsyncTask; -import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; -import code.name.monkey.retromusic.util.MusicUtil; -import code.name.monkey.retromusic.util.SAFUtil; +public class WriteTagsAsyncTask + extends DialogAsyncTask { -public class WriteTagsAsyncTask extends DialogAsyncTask { + private WeakReference activity; - private WeakReference activity; + public WriteTagsAsyncTask(@NonNull Activity activity) { + super(activity); + this.activity = new WeakReference<>(activity); + } - public WriteTagsAsyncTask(@NonNull Activity activity) { - super(activity); - this.activity = new WeakReference<>(activity); - } + @NonNull + @Override + protected Dialog createDialog(@NonNull Context context) { - @NonNull - @Override - protected Dialog createDialog(@NonNull Context context) { + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.saving_changes) + .setCancelable(false) + .setView(R.layout.loading) + .create(); + } - return new MaterialAlertDialogBuilder(context) - .setTitle(R.string.saving_changes) - .setCancelable(false) - .setView(R.layout.loading) - .create(); - } + @Override + protected String[] doInBackground(LoadingInfo... params) { + try { + LoadingInfo info = params[0]; - @Override - protected String[] doInBackground(LoadingInfo... params) { + Artwork artwork = null; + File albumArtFile = null; + if (info.artworkInfo != null && info.artworkInfo.getArtwork() != null) { try { - LoadingInfo info = params[0]; - - Artwork artwork = null; - File albumArtFile = null; - if (info.artworkInfo != null && info.artworkInfo.getArtwork() != null) { - try { - albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile(); - info.artworkInfo.getArtwork() - .compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile)); - artwork = ArtworkFactory.createArtworkFromFile(albumArtFile); - } catch (IOException e) { - e.printStackTrace(); - } - } - - int counter = 0; - boolean wroteArtwork = false; - boolean deletedArtwork = false; - for (String filePath : info.filePaths) { - publishProgress(++counter, info.filePaths.size()); - try { - Uri safUri = null; - if (filePath.contains(SAFUtil.SEPARATOR)) { - String[] fragments = filePath.split(SAFUtil.SEPARATOR); - filePath = fragments[0]; - safUri = Uri.parse(fragments[1]); - } - - AudioFile audioFile = AudioFileIO.read(new File(filePath)); - Tag tag = audioFile.getTagOrCreateAndSetDefault(); - - if (info.fieldKeyValueMap != null) { - for (Map.Entry entry : info.fieldKeyValueMap.entrySet()) { - try { - tag.setField(entry.getKey(), entry.getValue()); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - if (info.artworkInfo != null) { - if (info.artworkInfo.getArtwork() == null) { - tag.deleteArtworkField(); - deletedArtwork = true; - } else if (artwork != null) { - tag.deleteArtworkField(); - tag.setField(artwork); - wroteArtwork = true; - } - } - - Activity activity = this.activity.get(); - SAFUtil.write(activity, audioFile, safUri); - - } catch (@NonNull Exception e) { - e.printStackTrace(); - } - } - - Context context = getContext(); - if (context != null) { - if (wroteArtwork) { - MusicUtil.INSTANCE.insertAlbumArt(context, info.artworkInfo.getAlbumId(), albumArtFile.getPath()); - } else if (deletedArtwork) { - MusicUtil.INSTANCE.deleteAlbumArt(context, info.artworkInfo.getAlbumId()); - } - } - - Collection paths = info.filePaths; - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { - paths = new ArrayList<>(info.filePaths.size()); - for (String path : info.filePaths) { - if (path.contains(SAFUtil.SEPARATOR)) { - path = path.split(SAFUtil.SEPARATOR)[0]; - } - paths.add(path); - } - } - - return paths.toArray(new String[paths.size()]); - } catch (Exception e) { - e.printStackTrace(); - return null; + albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile(); + info.artworkInfo + .getArtwork() + .compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile)); + artwork = ArtworkFactory.createArtworkFromFile(albumArtFile); + } catch (IOException e) { + e.printStackTrace(); } - } + } - @Override - protected void onCancelled(String[] toBeScanned) { - super.onCancelled(toBeScanned); - scan(toBeScanned); - } + int counter = 0; + boolean wroteArtwork = false; + boolean deletedArtwork = false; + for (String filePath : info.filePaths) { + publishProgress(++counter, info.filePaths.size()); + try { + Uri safUri = null; + if (filePath.contains(SAFUtil.SEPARATOR)) { + String[] fragments = filePath.split(SAFUtil.SEPARATOR); + filePath = fragments[0]; + safUri = Uri.parse(fragments[1]); + } - @Override - protected void onPostExecute(String[] toBeScanned) { - super.onPostExecute(toBeScanned); - scan(toBeScanned); - } + AudioFile audioFile = AudioFileIO.read(new File(filePath)); + Tag tag = audioFile.getTagOrCreateAndSetDefault(); - @Override - protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) { - super.onProgressUpdate(dialog, values); - //((MaterialDialog) dialog).setMaxProgress(values[1]); - //((MaterialDialog) dialog).setProgress(values[0]); - } + if (info.fieldKeyValueMap != null) { + for (Map.Entry entry : info.fieldKeyValueMap.entrySet()) { + try { + tag.setField(entry.getKey(), entry.getValue()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } - private void scan(String[] toBeScanned) { - Activity activity = this.activity.get(); - if (activity != null) { - MediaScannerConnection.scanFile(activity, toBeScanned, null, - new UpdateToastMediaScannerCompletionListener(activity, toBeScanned)); + if (info.artworkInfo != null) { + if (info.artworkInfo.getArtwork() == null) { + tag.deleteArtworkField(); + deletedArtwork = true; + } else if (artwork != null) { + tag.deleteArtworkField(); + tag.setField(artwork); + wroteArtwork = true; + } + } + + Activity activity = this.activity.get(); + SAFUtil.write(activity, audioFile, safUri); + + } catch (@NonNull Exception e) { + e.printStackTrace(); } - } + } - public static class LoadingInfo { - - @Nullable - final Map fieldKeyValueMap; - - final Collection filePaths; - - @Nullable - private AbsTagEditorActivity.ArtworkInfo artworkInfo; - - public LoadingInfo(Collection filePaths, - @Nullable Map fieldKeyValueMap, - @Nullable AbsTagEditorActivity.ArtworkInfo artworkInfo) { - this.filePaths = filePaths; - this.fieldKeyValueMap = fieldKeyValueMap; - this.artworkInfo = artworkInfo; + Context context = getContext(); + if (context != null) { + if (wroteArtwork) { + MusicUtil.INSTANCE.insertAlbumArt( + context, info.artworkInfo.getAlbumId(), albumArtFile.getPath()); + } else if (deletedArtwork) { + MusicUtil.INSTANCE.deleteAlbumArt(context, info.artworkInfo.getAlbumId()); } + } + + Collection paths = info.filePaths; + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + paths = new ArrayList<>(info.filePaths.size()); + for (String path : info.filePaths) { + if (path.contains(SAFUtil.SEPARATOR)) { + path = path.split(SAFUtil.SEPARATOR)[0]; + } + paths.add(path); + } + } + + return paths.toArray(new String[paths.size()]); + } catch (Exception e) { + e.printStackTrace(); + return null; } -} \ No newline at end of file + } + + @Override + protected void onCancelled(String[] toBeScanned) { + super.onCancelled(toBeScanned); + scan(toBeScanned); + } + + @Override + protected void onPostExecute(String[] toBeScanned) { + super.onPostExecute(toBeScanned); + scan(toBeScanned); + } + + @Override + protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) { + super.onProgressUpdate(dialog, values); + // ((MaterialDialog) dialog).setMaxProgress(values[1]); + // ((MaterialDialog) dialog).setProgress(values[0]); + } + + private void scan(String[] toBeScanned) { + Activity activity = this.activity.get(); + if (activity != null) { + MediaScannerConnection.scanFile( + activity, + toBeScanned, + null, + new UpdateToastMediaScannerCompletionListener(activity, toBeScanned)); + } + } + + public static class LoadingInfo { + + @Nullable final Map fieldKeyValueMap; + + final Collection filePaths; + + @Nullable private AbsTagEditorActivity.ArtworkInfo artworkInfo; + + public LoadingInfo( + Collection filePaths, + @Nullable Map fieldKeyValueMap, + @Nullable AbsTagEditorActivity.ArtworkInfo artworkInfo) { + this.filePaths = filePaths; + this.fieldKeyValueMap = fieldKeyValueMap; + this.artworkInfo = artworkInfo; + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.java b/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.java index f079b3180..eb54a5ace 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.java +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.java @@ -22,116 +22,119 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.checkbox.MaterialCheckBox; - -import java.util.List; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.model.CategoryInfo; import code.name.monkey.retromusic.util.SwipeAndDragHelper; +import com.google.android.material.checkbox.MaterialCheckBox; +import java.util.List; public class CategoryInfoAdapter extends RecyclerView.Adapter - implements SwipeAndDragHelper.ActionCompletionContract { + implements SwipeAndDragHelper.ActionCompletionContract { - private List categoryInfos; - private ItemTouchHelper touchHelper; + private List categoryInfos; + private ItemTouchHelper touchHelper; - public CategoryInfoAdapter() { - SwipeAndDragHelper swipeAndDragHelper = new SwipeAndDragHelper(this); - touchHelper = new ItemTouchHelper(swipeAndDragHelper); - } + public CategoryInfoAdapter() { + SwipeAndDragHelper swipeAndDragHelper = new SwipeAndDragHelper(this); + touchHelper = new ItemTouchHelper(swipeAndDragHelper); + } - public void attachToRecyclerView(RecyclerView recyclerView) { - touchHelper.attachToRecyclerView(recyclerView); - } + public void attachToRecyclerView(RecyclerView recyclerView) { + touchHelper.attachToRecyclerView(recyclerView); + } - @NonNull - public List getCategoryInfos() { - return categoryInfos; - } + @NonNull + public List getCategoryInfos() { + return categoryInfos; + } - public void setCategoryInfos(@NonNull List categoryInfos) { - this.categoryInfos = categoryInfos; - notifyDataSetChanged(); - } + public void setCategoryInfos(@NonNull List categoryInfos) { + this.categoryInfos = categoryInfos; + notifyDataSetChanged(); + } - @Override - public int getItemCount() { - return categoryInfos.size(); - } + @Override + public int getItemCount() { + return categoryInfos.size(); + } - @SuppressLint("ClickableViewAccessibility") - @Override - public void onBindViewHolder(@NonNull CategoryInfoAdapter.ViewHolder holder, int position) { - CategoryInfo categoryInfo = categoryInfos.get(position); + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NonNull CategoryInfoAdapter.ViewHolder holder, int position) { + CategoryInfo categoryInfo = categoryInfos.get(position); - holder.checkBox.setChecked(categoryInfo.isVisible()); - holder.title.setText(holder.title.getResources().getString(categoryInfo.getCategory().getStringRes())); + holder.checkBox.setChecked(categoryInfo.isVisible()); + holder.title.setText( + holder.title.getResources().getString(categoryInfo.getCategory().getStringRes())); - holder.itemView.setOnClickListener(v -> { - if (!(categoryInfo.isVisible() && isLastCheckedCategory(categoryInfo))) { - categoryInfo.setVisible(!categoryInfo.isVisible()); - holder.checkBox.setChecked(categoryInfo.isVisible()); - } else { - Toast.makeText(holder.itemView.getContext(), R.string.you_have_to_select_at_least_one_category, - Toast.LENGTH_SHORT).show(); - } + holder.itemView.setOnClickListener( + v -> { + if (!(categoryInfo.isVisible() && isLastCheckedCategory(categoryInfo))) { + categoryInfo.setVisible(!categoryInfo.isVisible()); + holder.checkBox.setChecked(categoryInfo.isVisible()); + } else { + Toast.makeText( + holder.itemView.getContext(), + R.string.you_have_to_select_at_least_one_category, + Toast.LENGTH_SHORT) + .show(); + } }); - holder.dragView.setOnTouchListener((view, event) -> { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - touchHelper.startDrag(holder); - } - return false; - } - ); - } + holder.dragView.setOnTouchListener( + (view, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + touchHelper.startDrag(holder); + } + return false; + }); + } - @Override - @NonNull - public CategoryInfoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.preference_dialog_library_categories_listitem, parent, false); - return new ViewHolder(view); - } + @Override + @NonNull + public CategoryInfoAdapter.ViewHolder onCreateViewHolder( + @NonNull ViewGroup parent, int viewType) { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.preference_dialog_library_categories_listitem, parent, false); + return new ViewHolder(view); + } - @Override - public void onViewMoved(int oldPosition, int newPosition) { - CategoryInfo categoryInfo = categoryInfos.get(oldPosition); - categoryInfos.remove(oldPosition); - categoryInfos.add(newPosition, categoryInfo); - notifyItemMoved(oldPosition, newPosition); - } + @Override + public void onViewMoved(int oldPosition, int newPosition) { + CategoryInfo categoryInfo = categoryInfos.get(oldPosition); + categoryInfos.remove(oldPosition); + categoryInfos.add(newPosition, categoryInfo); + notifyItemMoved(oldPosition, newPosition); + } - private boolean isLastCheckedCategory(CategoryInfo categoryInfo) { - if (categoryInfo.isVisible()) { - for (CategoryInfo c : categoryInfos) { - if (c != categoryInfo && c.isVisible()) { - return false; - } - } + private boolean isLastCheckedCategory(CategoryInfo categoryInfo) { + if (categoryInfo.isVisible()) { + for (CategoryInfo c : categoryInfos) { + if (c != categoryInfo && c.isVisible()) { + return false; } - return true; + } } + return true; + } - static class ViewHolder extends RecyclerView.ViewHolder { - private MaterialCheckBox checkBox; - private View dragView; - private TextView title; + static class ViewHolder extends RecyclerView.ViewHolder { + private MaterialCheckBox checkBox; + private View dragView; + private TextView title; - ViewHolder(View view) { - super(view); - checkBox = view.findViewById(R.id.checkbox); - checkBox.setButtonTintList( - ColorStateList.valueOf(ThemeStore.Companion.accentColor(checkBox.getContext()))); - title = view.findViewById(R.id.title); - dragView = view.findViewById(R.id.drag_view); - } + ViewHolder(View view) { + super(view); + checkBox = view.findViewById(R.id.checkbox); + checkBox.setButtonTintList( + ColorStateList.valueOf(ThemeStore.Companion.accentColor(checkBox.getContext()))); + title = view.findViewById(R.id.title); + dragView = view.findViewById(R.id.drag_view); } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt index 84290ffe6..28719c962 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/ContributorAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter import android.app.Activity diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt index 595eafaea..2156b7c34 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter import android.view.LayoutInflater diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt index 86e272e4e..fef1c37d2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter import android.view.LayoutInflater @@ -121,7 +135,6 @@ class HomeAdapter( viewHolder.bind(home) } PLAYLISTS -> { - } } } @@ -181,7 +194,6 @@ class HomeAdapter( .asBitmap() .build() .into(itemView.findViewById(id)) - } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt index 6fe434c2c..9c527cccc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter import android.view.LayoutInflater @@ -60,7 +74,7 @@ class SearchAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { when (getItemViewType(position)) { ALBUM -> { - holder. imageTextContainer?.isVisible = true + holder.imageTextContainer?.isVisible = true val album = dataSet[position] as Album holder.title?.text = album.title holder.text?.text = album.artistName @@ -68,7 +82,7 @@ class SearchAdapter( .checkIgnoreMediaStore().build().into(holder.image) } ARTIST -> { - holder. imageTextContainer?.isVisible = true + holder.imageTextContainer?.isVisible = true val artist = dataSet[position] as Artist holder.title?.text = artist.name holder.text?.text = MusicUtil.getArtistInfoString(activity, artist) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt index 511336227..eabde5bfb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt @@ -1,17 +1,16 @@ /* - * Copyright 2019 Google LLC + * Copyright (c) 2020 Hemanth Savarla. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the GNU General Public License v3 * - * https://www.apache.org/licenses/LICENSE-2.0 + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package code.name.monkey.retromusic.adapter @@ -33,11 +32,11 @@ import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.signature.MediaStoreSignature -import me.zhanghai.android.fastscroll.PopupTextProvider import java.io.File import java.text.DecimalFormat import kotlin.math.log10 import kotlin.math.pow +import me.zhanghai.android.fastscroll.PopupTextProvider class SongFileAdapter( private val activity: AppCompatActivity, @@ -148,7 +147,6 @@ class SongFileAdapter( return MusicUtil.getSectionName(dataSet[position].name) } - inner class ViewHolder(itemView: View) : MediaEntryViewHolder(itemView) { init { @@ -198,4 +196,4 @@ class SongFileAdapter( return DecimalFormat("#,##0.##").format(size / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/TranslatorsAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/TranslatorsAdapter.kt index 90585c364..9630c25c4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/TranslatorsAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/TranslatorsAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter import android.app.Activity @@ -49,4 +63,4 @@ class TranslatorsAdapter( image.hide() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt index 2461fad5e..178d16afd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.album import android.content.res.ColorStateList @@ -129,7 +143,8 @@ open class AlbumAdapter( } override fun onMultipleItemAction( - menuItem: MenuItem, selection: List + menuItem: MenuItem, + selection: List ) { SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index d48fe2d44..d994c8243 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.album import android.content.Intent @@ -205,4 +219,3 @@ class AlbumCoverPagerAdapter( val TAG: String = AlbumCoverPagerAdapter::class.java.simpleName } } - diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt index 6f743b6b4..26a1958c7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.album import android.view.View @@ -29,8 +43,8 @@ class HorizontalAlbumAdapter( } override fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) { - //holder.title?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorPrimary)) - //holder.text?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorSecondary)) + // holder.title?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorPrimary)) + // holder.text?.setTextColor(ATHUtil.resolveColor(activity, android.R.attr.textColorSecondary)) } override fun loadAlbumCover(album: Album, holder: ViewHolder) { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt index e925576cc..7719166b6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.artist import android.content.res.ColorStateList @@ -22,8 +36,8 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide -import me.zhanghai.android.fastscroll.PopupTextProvider import java.util.* +import me.zhanghai.android.fastscroll.PopupTextProvider class ArtistAdapter( val activity: FragmentActivity, @@ -107,7 +121,8 @@ class ArtistAdapter( } override fun onMultipleItemAction( - menuItem: MenuItem, selection: List + menuItem: MenuItem, + selection: List ) { SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java index 0e2667ae6..f0d431adf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.java @@ -3,132 +3,127 @@ package code.name.monkey.retromusic.adapter.base; import android.content.Context; import android.view.Menu; import android.view.MenuItem; - import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.interfaces.ICabHolder; import com.afollestad.materialcab.MaterialCab; - import java.util.ArrayList; import java.util.List; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.interfaces.ICabHolder; +public abstract class AbsMultiSelectAdapter + extends RecyclerView.Adapter implements MaterialCab.Callback { + @Nullable private final ICabHolder ICabHolder; + private final Context context; + private MaterialCab cab; + private List checked; + private int menuRes; -public abstract class AbsMultiSelectAdapter extends RecyclerView.Adapter - implements MaterialCab.Callback { + public AbsMultiSelectAdapter( + @NonNull Context context, @Nullable ICabHolder ICabHolder, @MenuRes int menuRes) { + this.ICabHolder = ICabHolder; + checked = new ArrayList<>(); + this.menuRes = menuRes; + this.context = context; + } - @Nullable - private final ICabHolder ICabHolder; - private final Context context; - private MaterialCab cab; - private List checked; - private int menuRes; + @Override + public boolean onCabCreated(MaterialCab materialCab, Menu menu) { + return true; + } - public AbsMultiSelectAdapter(@NonNull Context context, @Nullable ICabHolder ICabHolder, @MenuRes int menuRes) { - this.ICabHolder = ICabHolder; - checked = new ArrayList<>(); - this.menuRes = menuRes; - this.context = context; + @Override + public boolean onCabFinished(MaterialCab materialCab) { + clearChecked(); + return true; + } + + @Override + public boolean onCabItemClicked(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.action_multi_select_adapter_check_all) { + checkAll(); + } else { + onMultipleItemAction(menuItem, new ArrayList<>(checked)); + cab.finish(); + clearChecked(); } + return true; + } - @Override - public boolean onCabCreated(MaterialCab materialCab, Menu menu) { - return true; - } - - @Override - public boolean onCabFinished(MaterialCab materialCab) { - clearChecked(); - return true; - } - - @Override - public boolean onCabItemClicked(MenuItem menuItem) { - if (menuItem.getItemId() == R.id.action_multi_select_adapter_check_all) { - checkAll(); - } else { - onMultipleItemAction(menuItem, new ArrayList<>(checked)); - cab.finish(); - clearChecked(); + protected void checkAll() { + if (ICabHolder != null) { + checked.clear(); + for (int i = 0; i < getItemCount(); i++) { + I identifier = getIdentifier(i); + if (identifier != null) { + checked.add(identifier); } - return true; + } + notifyDataSetChanged(); + updateCab(); } + } - protected void checkAll() { - if (ICabHolder != null) { - checked.clear(); - for (int i = 0; i < getItemCount(); i++) { - I identifier = getIdentifier(i); - if (identifier != null) { - checked.add(identifier); - } - } - notifyDataSetChanged(); - updateCab(); - } - } + @Nullable + protected abstract I getIdentifier(int position); - @Nullable - protected abstract I getIdentifier(int position); + protected String getName(I object) { + return object.toString(); + } - protected String getName(I object) { - return object.toString(); - } + protected boolean isChecked(I identifier) { + return checked.contains(identifier); + } - protected boolean isChecked(I identifier) { - return checked.contains(identifier); - } + protected boolean isInQuickSelectMode() { + return cab != null && cab.isActive(); + } - protected boolean isInQuickSelectMode() { - return cab != null && cab.isActive(); - } + protected abstract void onMultipleItemAction(MenuItem menuItem, List selection); - protected abstract void onMultipleItemAction(MenuItem menuItem, List selection); + protected void setMultiSelectMenuRes(@MenuRes int menuRes) { + this.menuRes = menuRes; + } - protected void setMultiSelectMenuRes(@MenuRes int menuRes) { - this.menuRes = menuRes; - } - - protected boolean toggleChecked(final int position) { - if (ICabHolder != null) { - I identifier = getIdentifier(position); - if (identifier == null) { - return false; - } - - if (!checked.remove(identifier)) { - checked.add(identifier); - } - - notifyItemChanged(position); - updateCab(); - return true; - } + protected boolean toggleChecked(final int position) { + if (ICabHolder != null) { + I identifier = getIdentifier(position); + if (identifier == null) { return false; - } + } - private void clearChecked() { - checked.clear(); - notifyDataSetChanged(); - } + if (!checked.remove(identifier)) { + checked.add(identifier); + } - private void updateCab() { - if (ICabHolder != null) { - if (cab == null || !cab.isActive()) { - cab = ICabHolder.openCab(menuRes, this); - } - final int size = checked.size(); - if (size <= 0) { - cab.finish(); - } else if (size == 1) { - cab.setTitle(getName(checked.get(0))); - } else { - cab.setTitle(context.getString(R.string.x_selected, size)); - } - } + notifyItemChanged(position); + updateCab(); + return true; } + return false; + } + + private void clearChecked() { + checked.clear(); + notifyDataSetChanged(); + } + + private void updateCab() { + if (ICabHolder != null) { + if (cab == null || !cab.isActive()) { + cab = ICabHolder.openCab(menuRes, this); + } + final int size = checked.size(); + if (size <= 0) { + cab.finish(); + } else if (size == 1) { + cab.setTitle(getName(checked.get(0))); + } else { + cab.setTitle(context.getString(R.string.x_selected, size)); + } + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java b/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java index ae5587238..903476163 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java @@ -19,123 +19,101 @@ import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - +import code.name.monkey.retromusic.R; import com.google.android.material.card.MaterialCardView; import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder; -import code.name.monkey.retromusic.R; - public class MediaEntryViewHolder extends AbstractDraggableSwipeableItemViewHolder - implements View.OnLongClickListener, View.OnClickListener { + implements View.OnLongClickListener, View.OnClickListener { - @Nullable - public View dragView; + @Nullable public View dragView; - @Nullable - public View dummyContainer; + @Nullable public View dummyContainer; - @Nullable - public ImageView image; + @Nullable public ImageView image; - @Nullable - public ImageView artistImage; + @Nullable public ImageView artistImage; - @Nullable - public ImageView playerImage; + @Nullable public ImageView playerImage; - @Nullable - public MaterialCardView imageContainerCard; + @Nullable public MaterialCardView imageContainerCard; - @Nullable - public TextView imageText; + @Nullable public TextView imageText; - @Nullable - public MaterialCardView imageTextContainer; + @Nullable public MaterialCardView imageTextContainer; - @Nullable - public View mask; + @Nullable public View mask; - @Nullable - public View menu; + @Nullable public View menu; - @Nullable - public View paletteColorContainer; + @Nullable public View paletteColorContainer; - @Nullable - public ImageButton playSongs; + @Nullable public ImageButton playSongs; - @Nullable - public RecyclerView recyclerView; + @Nullable public RecyclerView recyclerView; - @Nullable - public TextView text; + @Nullable public TextView text; - @Nullable - public TextView text2; + @Nullable public TextView text2; - @Nullable - public TextView time; + @Nullable public TextView time; - @Nullable - public TextView title; + @Nullable public TextView title; - public MediaEntryViewHolder(@NonNull View itemView) { - super(itemView); - title = itemView.findViewById(R.id.title); - text = itemView.findViewById(R.id.text); - text2 = itemView.findViewById(R.id.text2); + public MediaEntryViewHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.title); + text = itemView.findViewById(R.id.text); + text2 = itemView.findViewById(R.id.text2); - image = itemView.findViewById(R.id.image); - artistImage = itemView.findViewById(R.id.artistImage); - playerImage = itemView.findViewById(R.id.player_image); - time = itemView.findViewById(R.id.time); + image = itemView.findViewById(R.id.image); + artistImage = itemView.findViewById(R.id.artistImage); + playerImage = itemView.findViewById(R.id.player_image); + time = itemView.findViewById(R.id.time); - imageText = itemView.findViewById(R.id.imageText); - imageTextContainer = itemView.findViewById(R.id.imageTextContainer); - imageContainerCard = itemView.findViewById(R.id.imageContainerCard); + imageText = itemView.findViewById(R.id.imageText); + imageTextContainer = itemView.findViewById(R.id.imageTextContainer); + imageContainerCard = itemView.findViewById(R.id.imageContainerCard); - menu = itemView.findViewById(R.id.menu); - dragView = itemView.findViewById(R.id.drag_view); - paletteColorContainer = itemView.findViewById(R.id.paletteColorContainer); - recyclerView = itemView.findViewById(R.id.recycler_view); - mask = itemView.findViewById(R.id.mask); - playSongs = itemView.findViewById(R.id.playSongs); - dummyContainer = itemView.findViewById(R.id.dummy_view); + menu = itemView.findViewById(R.id.menu); + dragView = itemView.findViewById(R.id.drag_view); + paletteColorContainer = itemView.findViewById(R.id.paletteColorContainer); + recyclerView = itemView.findViewById(R.id.recycler_view); + mask = itemView.findViewById(R.id.mask); + playSongs = itemView.findViewById(R.id.playSongs); + dummyContainer = itemView.findViewById(R.id.dummy_view); - if (imageContainerCard != null) { - imageContainerCard.setCardBackgroundColor(Color.TRANSPARENT); - } - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); + if (imageContainerCard != null) { + imageContainerCard.setCardBackgroundColor(Color.TRANSPARENT); } + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + } - @Nullable - @Override - public View getSwipeableContainerView() { - return null; - } - - @Override - public void onClick(View v) { - - } - - @Override - public boolean onLongClick(View v) { - return false; - } - - public void setImageTransitionName(@NonNull String transitionName) { - itemView.setTransitionName(transitionName); - /* if (imageContainerCard != null) { - imageContainerCard.setTransitionName(transitionName); - } - if (image != null) { - image.setTransitionName(transitionName); - }*/ + @Nullable + @Override + public View getSwipeableContainerView() { + return null; + } + + @Override + public void onClick(View v) {} + + @Override + public boolean onLongClick(View v) { + return false; + } + + public void setImageTransitionName(@NonNull String transitionName) { + itemView.setTransitionName(transitionName); + /* if (imageContainerCard != null) { + imageContainerCard.setTransitionName(transitionName); } + if (image != null) { + image.setTransitionName(transitionName); + }*/ + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/LegacyPlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/LegacyPlaylistAdapter.kt index c74c8a5f9..9372e507f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/LegacyPlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/LegacyPlaylistAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.playlist import android.view.LayoutInflater @@ -49,4 +63,4 @@ class LegacyPlaylistAdapter( interface PlaylistClickListener { fun onPlaylistClick(playlist: Playlist) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt index 85c34eecd..ddfe8c0c6 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.playlist import android.graphics.Bitmap @@ -56,7 +70,7 @@ class PlaylistAdapter( } override fun getItemId(position: Int): Long { - return dataSet[position].playlistEntity.playListId.toLong() + return dataSet[position].playlistEntity.playListId } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -88,7 +102,7 @@ class PlaylistAdapter( } else { holder.menu?.show() } - //PlaylistBitmapLoader(this, holder, playlist).execute() + // PlaylistBitmapLoader(this, holder, playlist).execute() } private fun getIconRes(): Drawable = TintHelper.createTintedDrawable( diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt index 35cf5791d..03ed72337 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.LayoutInflater @@ -76,4 +90,4 @@ abstract class AbsOffsetSongAdapter( const val OFFSET_ITEM = 0 const val SONG = 1 } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt index 64f9aa199..eafb93f42 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.MenuItem diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt index 7004909b8..a6e986229 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.MenuItem @@ -196,17 +210,17 @@ class PlayingQueueAdapter( private val isPlaying: Boolean = MusicPlayerRemote.isPlaying private val songProgressMillis = 0 override fun onPerformAction() { - //currentlyShownSnackbar = null + // currentlyShownSnackbar = null } override fun onSlideAnimationEnd() { - //initializeSnackBar(adapter, position, activity, isPlaying) + // initializeSnackBar(adapter, position, activity, isPlaying) songToRemove = adapter.dataSet[position] - //If song removed was the playing song, then play the next song + // If song removed was the playing song, then play the next song if (isPlaying(songToRemove!!)) { playNextSong() } - //Swipe animation is much smoother when we do the heavy lifting after it's completed + // Swipe animation is much smoother when we do the heavy lifting after it's completed adapter.setSongToRemove(songToRemove!!) removeFromQueue(songToRemove!!) } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt index 0153f0e83..612abcd52 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlaylistSongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.MenuItem @@ -45,4 +59,4 @@ open class PlaylistSongAdapter( return super.onSongMenuItemClick(item) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt index 794cb8a4c..8eda478eb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.View @@ -49,4 +63,4 @@ class ShuffleButtonSongAdapter( super.onClick(v) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt index 7bfc4a1c1..5d3e62638 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.view.LayoutInflater diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index a6b6fe66b..8c2f642e6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.adapter.song import android.content.res.ColorStateList diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutIconGenerator.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutIconGenerator.kt index ff4dbf5af..3186d609c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutIconGenerator.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutIconGenerator.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts import android.content.Context @@ -60,7 +60,10 @@ object AppShortcutIconGenerator { } private fun generateThemedIcon( - context: Context, iconId: Int, foregroundColor: Int, backgroundColor: Int + context: Context, + iconId: Int, + foregroundColor: Int, + backgroundColor: Int ): Icon { // Get and tint foreground and background drawables val vectorDrawable = RetroUtil.getTintedVectorDrawable(context, iconId, foregroundColor) diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt index f41e6946d..68d04c479 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts import android.app.Activity diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt index c5f0ed6dc..7de8c8eff 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts import android.annotation.TargetApi @@ -39,9 +39,9 @@ class DynamicShortcutManager(private val context: Context) { ) fun initDynamicShortcuts() { - //if (shortcutManager.dynamicShortcuts.size == 0) { + // if (shortcutManager.dynamicShortcuts.size == 0) { shortcutManager.dynamicShortcuts = defaultShortcuts - //} + // } } fun updateDynamicShortcuts() { diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt index c2b376ded..1be5edcbf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts.shortcuttype import android.annotation.TargetApi diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/LastAddedShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/LastAddedShortcutType.kt index ef80bc0ec..4855f22ed 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/LastAddedShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/LastAddedShortcutType.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts.shortcuttype import android.annotation.TargetApi diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt index dc86fa830..4d13055ec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts.shortcuttype import android.annotation.TargetApi diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/TopTracksShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/TopTracksShortcutType.kt index 1317977c8..ab814f4af 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/TopTracksShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/TopTracksShortcutType.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appshortcuts.shortcuttype import android.annotation.TargetApi diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt index 0a3b3c2c6..6bad7c4c9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.app.PendingIntent @@ -229,6 +229,5 @@ class AppWidgetBig : BaseAppWidget() { } return mInstance!! } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt index e4702eb5d..7e756d6d8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.app.PendingIntent diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt index 0cbff1cbb..b57cc91c8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.app.PendingIntent @@ -49,7 +49,6 @@ class AppWidgetClassic : BaseAppWidget() { override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) { val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_classic) - appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE) appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art) appWidgetView.setImageViewBitmap( diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt index 70c1026d8..f09b48491 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.app.PendingIntent diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt index 777606ad7..a09e3ff2d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.app.PendingIntent @@ -153,10 +153,7 @@ class AppWidgetText : BaseAppWidget() { ) ) - - pushUpdate(service.applicationContext, appWidgetIds, appWidgetView) - } companion object { diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/BootReceiver.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/BootReceiver.kt index 5f82c2af6..079e90ae6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/BootReceiver.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/BootReceiver.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets import android.appwidget.AppWidgetManager diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt index 29f0d7481..5172a76b5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.appwidgets.base import android.app.PendingIntent @@ -40,7 +40,9 @@ abstract class BaseAppWidget : AppWidgetProvider() { * {@inheritDoc} */ override fun onUpdate( - context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray ) { defaultAppWidget(context, appWidgetIds) val updateIntent = Intent(APP_WIDGET_UPDATE) @@ -62,7 +64,9 @@ abstract class BaseAppWidget : AppWidgetProvider() { } protected fun pushUpdate( - context: Context, appWidgetIds: IntArray?, views: RemoteViews + context: Context, + appWidgetIds: IntArray?, + views: RemoteViews ) { val appWidgetManager = AppWidgetManager.getInstance(context) if (appWidgetIds != null) { @@ -86,7 +90,9 @@ abstract class BaseAppWidget : AppWidgetProvider() { } protected fun buildPendingIntent( - context: Context, action: String, serviceName: ComponentName + context: Context, + action: String, + serviceName: ComponentName ): PendingIntent { val intent = Intent(action) intent.component = serviceName @@ -169,7 +175,11 @@ abstract class BaseAppWidget : AppWidgetProvider() { } protected fun composeRoundedRectPath( - rect: RectF, tl: Float, tr: Float, bl: Float, br: Float + rect: RectF, + tl: Float, + tr: Float, + bl: Float, + br: Float ): Path { val path = Path() path.moveTo(rect.left + tl, rect.top) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreDao.kt b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreDao.kt index db0dd0f64..0bf40b736 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreDao.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreDao.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.* @@ -18,4 +32,4 @@ interface BlackListStoreDao { @Query("SELECT * FROM BlackListStoreEntity") fun blackListPaths(): List -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreEntity.kt index 5ccbce07c..8592442a2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.Entity @@ -7,4 +21,4 @@ import androidx.room.PrimaryKey class BlackListStoreEntity( @PrimaryKey val path: String -) \ No newline at end of file +) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/HistoryDao.kt b/app/src/main/java/code/name/monkey/retromusic/db/HistoryDao.kt index 97288235d..7f6a64e62 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/HistoryDao.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/HistoryDao.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.lifecycle.LiveData @@ -23,4 +37,4 @@ interface HistoryDao { @Query("SELECT * FROM HistoryEntity ORDER BY time_played DESC LIMIT $HISTORY_LIMIT") fun observableHistorySongs(): LiveData> -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/HistoryEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/HistoryEntity.kt index a4facd796..535a37964 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/HistoryEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/HistoryEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.ColumnInfo @@ -29,4 +43,4 @@ class HistoryEntity( val albumArtist: String?, @ColumnInfo(name = "time_played") val timePlayed: Long -) \ No newline at end of file +) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/LyricsDao.kt b/app/src/main/java/code/name/monkey/retromusic/db/LyricsDao.kt index a09b430a2..fa14b1aca 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/LyricsDao.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/LyricsDao.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.* @@ -15,4 +29,4 @@ interface LyricsDao { @Update fun updateLyrics(lyricsEntity: LyricsEntity) -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/LyricsEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/LyricsEntity.kt index 0cec6431c..91d987d62 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/LyricsEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/LyricsEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.Entity @@ -7,4 +21,4 @@ import androidx.room.PrimaryKey class LyricsEntity( @PrimaryKey val songId: Int, val lyrics: String -) \ No newline at end of file +) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/PlayCountDao.kt b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountDao.kt index 4107b7ba5..420b2d434 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/PlayCountDao.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountDao.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.* @@ -24,4 +38,4 @@ interface PlayCountDao { @Query("UPDATE PlayCountEntity SET play_count = play_count + 1 WHERE id = :id") fun updateQuantity(id: Long) -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/PlayCountEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountEntity.kt index 0fe6c0885..2fa41b227 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/PlayCountEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.ColumnInfo @@ -31,4 +45,4 @@ class PlayCountEntity( val timePlayed: Long, @ColumnInfo(name = "play_count") var playCount: Int -) \ No newline at end of file +) diff --git a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistDao.kt b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistDao.kt index 36a4e8330..579777b9f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistDao.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistDao.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.lifecycle.LiveData @@ -45,12 +59,9 @@ interface PlaylistDao { @Delete suspend fun deletePlaylistSongs(songs: List) - @Query("SELECT * FROM SongEntity WHERE playlist_creator_id= :playlistId") fun favoritesSongsLiveData(playlistId: Long): LiveData> @Query("SELECT * FROM SongEntity WHERE playlist_creator_id= :playlistId") fun favoritesSongs(playlistId: Long): List - - -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistEntity.kt index 236e9cb40..cda8bff9f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import android.os.Parcelable @@ -14,4 +28,4 @@ class PlaylistEntity( val playListId: Long = 0, @ColumnInfo(name = "playlist_name") val playlistName: String -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistWithSongs.kt b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistWithSongs.kt index 5a256bde7..63c82e39a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/PlaylistWithSongs.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistWithSongs.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import android.os.Parcelable @@ -14,4 +28,3 @@ data class PlaylistWithSongs( ) val songs: List ) : Parcelable - diff --git a/app/src/main/java/code/name/monkey/retromusic/db/RetroDatabase.kt b/app/src/main/java/code/name/monkey/retromusic/db/RetroDatabase.kt index 6be545b6a..42eefa7b4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/RetroDatabase.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/RetroDatabase.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import androidx.room.Database @@ -14,4 +28,4 @@ abstract class RetroDatabase : RoomDatabase() { abstract fun playCountDao(): PlayCountDao abstract fun historyDao(): HistoryDao abstract fun lyricsDao(): LyricsDao -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/db/SongEntity.kt b/app/src/main/java/code/name/monkey/retromusic/db/SongEntity.kt index 6a6d236a3..279460282 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/SongEntity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/SongEntity.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import android.os.Parcelable @@ -36,4 +50,3 @@ class SongEntity( @ColumnInfo(name = "album_artist") val albumArtist: String? ) : Parcelable - diff --git a/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt b/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt index ed1ea3bea..b6d9f40d4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt +++ b/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.db import code.name.monkey.retromusic.model.Song @@ -137,4 +151,3 @@ fun List.toSongsEntity(playlistEntity: PlaylistEntity): List { it.toSongEntity(playlistEntity.playListId) } } - diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt index fc5d6a588..1dab03d46 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -47,7 +61,6 @@ class AddToPlaylistDialog : DialogFragment() { return adapter } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val playlistEntities = extraNotNull>(EXTRA_PLAYLISTS).value val songs = extraNotNull>(EXTRA_SONG).value @@ -77,4 +90,4 @@ class AddToPlaylistDialog : DialogFragment() { private fun showCreateDialog(songs: List) { CreatePlaylistDialog.create(songs).show(requireActivity().supportFragmentManager, "Dialog") } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.java b/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.java index 1e2099cc1..3a15c2d8c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.java +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.java @@ -7,150 +7,149 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.view.View; - import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.fragment.app.DialogFragment; - +import code.name.monkey.retromusic.R; import com.afollestad.materialdialogs.MaterialDialog; - import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import code.name.monkey.retromusic.R; +public class BlacklistFolderChooserDialog extends DialogFragment + implements MaterialDialog.ListCallback { -public class BlacklistFolderChooserDialog extends DialogFragment implements MaterialDialog.ListCallback { + String initialPath = Environment.getExternalStorageDirectory().getAbsolutePath(); + private File parentFolder; + private File[] parentContents; + private boolean canGoUp = false; + private FolderCallback callback; - String initialPath = Environment.getExternalStorageDirectory().getAbsolutePath(); - private File parentFolder; - private File[] parentContents; - private boolean canGoUp = false; - private FolderCallback callback; + public static BlacklistFolderChooserDialog create() { + return new BlacklistFolderChooserDialog(); + } - public static BlacklistFolderChooserDialog create() { - return new BlacklistFolderChooserDialog(); + private String[] getContentsArray() { + if (parentContents == null) { + if (canGoUp) { + return new String[] {".."}; + } + return new String[] {}; } - - private String[] getContentsArray() { - if (parentContents == null) { - if (canGoUp) { - return new String[]{".."}; - } - return new String[]{}; - } - String[] results = new String[parentContents.length + (canGoUp ? 1 : 0)]; - if (canGoUp) { - results[0] = ".."; - } - for (int i = 0; i < parentContents.length; i++) { - results[canGoUp ? i + 1 : i] = parentContents[i].getName(); - } - return results; + String[] results = new String[parentContents.length + (canGoUp ? 1 : 0)]; + if (canGoUp) { + results[0] = ".."; } - - private File[] listFiles() { - File[] contents = parentFolder.listFiles(); - List results = new ArrayList<>(); - if (contents != null) { - for (File fi : contents) { - if (fi.isDirectory()) { - results.add(fi); - } - } - Collections.sort(results, new FolderSorter()); - return results.toArray(new File[results.size()]); - } - return null; + for (int i = 0; i < parentContents.length; i++) { + results[canGoUp ? i + 1 : i] = parentContents[i].getName(); } + return results; + } - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && ActivityCompat.checkSelfPermission( + private File[] listFiles() { + File[] contents = parentFolder.listFiles(); + List results = new ArrayList<>(); + if (contents != null) { + for (File fi : contents) { + if (fi.isDirectory()) { + results.add(fi); + } + } + Collections.sort(results, new FolderSorter()); + return results.toArray(new File[results.size()]); + } + return null; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && ActivityCompat.checkSelfPermission( requireActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - return new MaterialDialog.Builder(requireActivity()) - .title(R.string.md_error_label) - .content(R.string.md_storage_perm_error) - .positiveText(android.R.string.ok) - .build(); - } - if (savedInstanceState == null) { - savedInstanceState = new Bundle(); - } - if (!savedInstanceState.containsKey("current_path")) { - savedInstanceState.putString("current_path", initialPath); - } - parentFolder = new File(savedInstanceState.getString("current_path", File.pathSeparator)); - checkIfCanGoUp(); - parentContents = listFiles(); - MaterialDialog.Builder builder = new MaterialDialog.Builder(requireContext()) - .title(parentFolder.getAbsolutePath()) - .items((CharSequence[]) getContentsArray()) - .itemsCallback(this) - .autoDismiss(false) - .onPositive((dialog, which) -> { - callback.onFolderSelection(BlacklistFolderChooserDialog.this, parentFolder); - dismiss(); + != PackageManager.PERMISSION_GRANTED) { + return new MaterialDialog.Builder(requireActivity()) + .title(R.string.md_error_label) + .content(R.string.md_storage_perm_error) + .positiveText(android.R.string.ok) + .build(); + } + if (savedInstanceState == null) { + savedInstanceState = new Bundle(); + } + if (!savedInstanceState.containsKey("current_path")) { + savedInstanceState.putString("current_path", initialPath); + } + parentFolder = new File(savedInstanceState.getString("current_path", File.pathSeparator)); + checkIfCanGoUp(); + parentContents = listFiles(); + MaterialDialog.Builder builder = + new MaterialDialog.Builder(requireContext()) + .title(parentFolder.getAbsolutePath()) + .items((CharSequence[]) getContentsArray()) + .itemsCallback(this) + .autoDismiss(false) + .onPositive( + (dialog, which) -> { + callback.onFolderSelection(BlacklistFolderChooserDialog.this, parentFolder); + dismiss(); }) - .onNegative((materialDialog, dialogAction) -> dismiss()) - .positiveText(R.string.add_action) - .negativeText(android.R.string.cancel); - return builder.build(); + .onNegative((materialDialog, dialogAction) -> dismiss()) + .positiveText(R.string.add_action) + .negativeText(android.R.string.cancel); + return builder.build(); + } + + @Override + public void onSelection(MaterialDialog materialDialog, View view, int i, CharSequence s) { + if (canGoUp && i == 0) { + parentFolder = parentFolder.getParentFile(); + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = parentFolder.getParentFile(); + } + checkIfCanGoUp(); + } else { + parentFolder = parentContents[canGoUp ? i - 1 : i]; + canGoUp = true; + if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { + parentFolder = Environment.getExternalStorageDirectory(); + } } + reload(); + } + + private void checkIfCanGoUp() { + canGoUp = parentFolder.getParent() != null; + } + + private void reload() { + parentContents = listFiles(); + MaterialDialog dialog = (MaterialDialog) getDialog(); + dialog.setTitle(parentFolder.getAbsolutePath()); + dialog.setItems((CharSequence[]) getContentsArray()); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("current_path", parentFolder.getAbsolutePath()); + } + + public void setCallback(FolderCallback callback) { + this.callback = callback; + } + + public interface FolderCallback { + void onFolderSelection(@NonNull BlacklistFolderChooserDialog dialog, @NonNull File folder); + } + + private static class FolderSorter implements Comparator { @Override - public void onSelection(MaterialDialog materialDialog, View view, int i, CharSequence s) { - if (canGoUp && i == 0) { - parentFolder = parentFolder.getParentFile(); - if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { - parentFolder = parentFolder.getParentFile(); - } - checkIfCanGoUp(); - } else { - parentFolder = parentContents[canGoUp ? i - 1 : i]; - canGoUp = true; - if (parentFolder.getAbsolutePath().equals("/storage/emulated")) { - parentFolder = Environment.getExternalStorageDirectory(); - } - } - reload(); + public int compare(File lhs, File rhs) { + return lhs.getName().compareTo(rhs.getName()); } - - private void checkIfCanGoUp() { - canGoUp = parentFolder.getParent() != null; - } - - private void reload() { - parentContents = listFiles(); - MaterialDialog dialog = (MaterialDialog) getDialog(); - dialog.setTitle(parentFolder.getAbsolutePath()); - dialog.setItems((CharSequence[]) getContentsArray()); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString("current_path", parentFolder.getAbsolutePath()); - } - - public void setCallback(FolderCallback callback) { - this.callback = callback; - } - - public interface FolderCallback { - void onFolderSelection(@NonNull BlacklistFolderChooserDialog dialog, @NonNull File folder); - } - - private static class FolderSorter implements Comparator { - - @Override - public int compare(File lhs, File rhs) { - return lhs.getName().compareTo(rhs.getName()); - } - } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt index d1e86e12e..052e3a1d8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -72,4 +86,4 @@ class CreatePlaylistDialog : DialogFragment() { .create() .colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeletePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeletePlaylistDialog.kt index 1ee8953ae..bfd321ccc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeletePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeletePlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -65,5 +79,4 @@ class DeletePlaylistDialog : DialogFragment() { .create() .colorButtons() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt index 24a67a0f1..2d7a9fc19 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/DeleteSongsDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -67,4 +81,4 @@ class DeleteSongsDialog : DialogFragment() { .create() .colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/ImportPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/ImportPlaylistDialog.kt index 359ef1c5b..1429aa5a1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/ImportPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/ImportPlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -21,4 +35,4 @@ class ImportPlaylistDialog : DialogFragment() { .create() .colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt index b52e9fef5..93ac92f85 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -66,4 +80,4 @@ class RemoveSongFromPlaylistDialog : DialogFragment() { .create() .colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/RenamePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/RenamePlaylistDialog.kt index 6aa6939ab..79b7d2501 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/RenamePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/RenamePlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -54,4 +68,4 @@ class RenamePlaylistDialog : DialogFragment() { .create() .colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt index bf726887b..62770536b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SavePlaylistDialog.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.dialogs import android.app.Dialog @@ -19,7 +33,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext - class SavePlaylistDialog : DialogFragment() { companion object { fun create(playlistWithSongs: PlaylistWithSongs): SavePlaylistDialog { @@ -41,7 +54,6 @@ class SavePlaylistDialog : DialogFragment() { arrayOf(file.path), null ) { _, _ -> - } withContext(Dispatchers.Main) { Toast.makeText( @@ -59,4 +71,4 @@ class SavePlaylistDialog : DialogFragment() { .setView(R.layout.loading) .create().colorButtons() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt index 21c43f303..3c3fd6e2c 100755 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.dialogs import android.annotation.SuppressLint @@ -130,7 +130,6 @@ class SleepTimerDialog : DialogFragment() { } .create() .colorButtons() - } private fun updateTimeDisplayTime() { @@ -173,4 +172,4 @@ class SleepTimerDialog : DialogFragment() { updateCancelButton() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SongDetailDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SongDetailDialog.kt index 3409b698b..72e548a84 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SongDetailDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SongDetailDialog.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.dialogs import android.annotation.SuppressLint @@ -31,13 +31,13 @@ import code.name.monkey.retromusic.extensions.colorButtons import code.name.monkey.retromusic.extensions.materialDialog import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil +import java.io.File +import java.io.IOException import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.audio.exceptions.CannotReadException import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException import org.jaudiotagger.audio.exceptions.ReadOnlyFileException import org.jaudiotagger.tag.TagException -import java.io.File -import java.io.IOException class SongDetailDialog : DialogFragment() { diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SongShareDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SongShareDialog.kt index f3ee3d6c0..1a5fbca60 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SongShareDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SongShareDialog.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.dialogs import android.app.Dialog diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt index 843cae01e..718e5a9fb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.extensions import android.app.Activity @@ -33,4 +33,4 @@ inline fun Activity.extra(key: String, default: T? = null) = l inline fun Activity.extraNotNull(key: String, default: T? = null) = lazy { val value = intent?.extras?.get(key) requireNotNull(if (value is T) value else default) { key } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt index 242bb2879..ef6085c44 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.extensions import android.app.Dialog @@ -180,7 +180,6 @@ fun ProgressIndicator.applyColor(color: Int) { } fun TextInputEditText.accentColor() { - } fun AppCompatImageView.accentColor(): Int { @@ -203,4 +202,3 @@ fun Drawable.tint(context: Context, @ColorRes color: Int): Drawable { fun Context.getColorCompat(@ColorRes colorRes: Int): Int { return ContextCompat.getColor(this, colorRes) } - diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/CursorExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/CursorExtensions.kt index 22c8e8667..a72eb155c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/CursorExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/CursorExtensions.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import android.database.Cursor @@ -34,4 +48,4 @@ internal fun Cursor.getStringOrNull(columnName: String): String? { } catch (ex: Throwable) { throw IllegalStateException("invalid column $columnName", ex) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/DialogExtension.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/DialogExtension.kt index c56a40459..c25c15e94 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/DialogExtension.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/DialogExtension.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import androidx.appcompat.app.AlertDialog @@ -19,4 +33,4 @@ fun AlertDialog.colorButtons(): AlertDialog { getButton(AlertDialog.BUTTON_NEUTRAL).accentTextColor() } return this -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/DimenExtension.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/DimenExtension.kt index f02e51875..6e5cdeccc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/DimenExtension.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/DimenExtension.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import android.app.Activity @@ -17,4 +31,4 @@ fun Activity.dipToPix(dpInFloat: Float): Float { fun Fragment.dipToPix(dpInFloat: Float): Float { val scale = resources.displayMetrics.density return dpInFloat * scale + 0.5f -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/DrawableExt.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/DrawableExt.kt index dc86fb949..3281c57b1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/DrawableExt.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/DrawableExt.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.extensions import android.content.Context @@ -59,4 +59,4 @@ fun getAdaptiveIconDrawable(context: Context): Drawable { } else { ContextCompat.getDrawable(context, R.drawable.color_circle_gradient)!! } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/FragmentExt.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/FragmentExt.kt index a826d1535..6c74050af 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/FragmentExt.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/FragmentExt.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import android.content.Context @@ -27,7 +41,6 @@ fun Context.getIntRes(@IntegerRes int: Int): Int { val Context.generalThemeValue get() = PreferenceUtil.getGeneralThemeValue(isSystemDarkModeEnabled()) - fun Context.isSystemDarkModeEnabled(): Boolean { val isBatterySaverEnabled = (getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isPowerSaveMode ?: false @@ -36,7 +49,6 @@ fun Context.isSystemDarkModeEnabled(): Boolean { return isBatterySaverEnabled or isDarkModeEnabled } - inline fun Fragment.extra(key: String, default: T? = null) = lazy { val value = arguments?.get(key) if (value is T) value else default @@ -84,4 +96,4 @@ fun Context.getDrawableCompat(@DrawableRes drawableRes: Int): Drawable { fun Fragment.getDrawableCompat(@DrawableRes drawableRes: Int): Drawable { return AppCompatResources.getDrawable(requireContext(), drawableRes)!! -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/NavigationExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/NavigationExtensions.kt index 2ea5ade8c..6bb0cebc2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/NavigationExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/NavigationExtensions.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import androidx.annotation.IdRes @@ -22,4 +36,4 @@ fun Fragment.findActivityNavController(@IdRes id: Int): NavController { fun AppCompatActivity.findNavController(@IdRes id: Int): NavController { val fragment = supportFragmentManager.findFragmentById(id) as NavHostFragment return fragment.navController -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/PaletteEX.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/PaletteEX.kt index bcc9b632c..58a631953 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/PaletteEX.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/PaletteEX.kt @@ -1,10 +1,23 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import androidx.annotation.ColorInt import androidx.core.graphics.ColorUtils import androidx.palette.graphics.Palette - fun getSuitableColorFor(palette: Palette, i: Int, i2: Int): Int { val dominantSwatch = palette.dominantSwatch if (dominantSwatch != null) { diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/Preference.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/Preference.kt index 0fe53f4a1..317844afd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/Preference.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/Preference.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.extensions import android.content.SharedPreferences diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 1e78b6902..674610d6b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.extensions import android.animation.ObjectAnimator @@ -50,8 +50,7 @@ fun EditText.appHandleColor(): EditText { return this } - -fun View.translateXAnimate(value: Float) { +fun View.translateXAnimate(value: Float) { ObjectAnimator.ofFloat(this, "translationY", value) .apply { duration = 300 @@ -76,4 +75,3 @@ fun BottomSheetBehavior<*>.peekHeightAnimate(value: Int) { start() } } - diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/AlbumCoverStyle.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/AlbumCoverStyle.kt index b8746b2c6..e0da98caa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/AlbumCoverStyle.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/AlbumCoverStyle.kt @@ -1,10 +1,23 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import androidx.annotation.DrawableRes import androidx.annotation.StringRes import code.name.monkey.retromusic.R - enum class AlbumCoverStyle( @StringRes val titleRes: Int, @DrawableRes val drawableResId: Int, diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/CoroutineViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/CoroutineViewModel.kt index 9b6a5ca4c..a1227d9c9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/CoroutineViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/CoroutineViewModel.kt @@ -1,8 +1,22 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import androidx.lifecycle.ViewModel -import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.* open class CoroutineViewModel( private val mainDispatcher: CoroutineDispatcher @@ -20,4 +34,4 @@ open class CoroutineViewModel( super.onCleared() job.cancel() } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index 361ba100f..7037909c9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import android.os.Bundle @@ -193,4 +207,4 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de ) ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 1fb477633..6a5548548 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import androidx.lifecycle.* @@ -166,7 +180,6 @@ class LibraryViewModel( override fun onPlayingMetaChanged() { println("onPlayingMetaChanged") - } override fun onPlayStateChanged() { @@ -299,4 +312,4 @@ enum class ReloadType { HomeSections, Playlists, Genres, -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/MiniPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/MiniPlayerFragment.kt index 9a16d6566..399f5ed1a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/MiniPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/MiniPlayerFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import android.animation.ObjectAnimator @@ -12,15 +26,19 @@ import android.view.MotionEvent import android.view.View import android.view.animation.DecelerateInterpolator import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.extensions.* +import code.name.monkey.retromusic.extensions.accentColor +import code.name.monkey.retromusic.extensions.applyColor +import code.name.monkey.retromusic.extensions.show +import code.name.monkey.retromusic.extensions.textColorPrimary +import code.name.monkey.retromusic.extensions.textColorSecondary import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper import code.name.monkey.retromusic.helper.PlayPauseButtonOnClickHandler import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil -import kotlinx.android.synthetic.main.fragment_mini_player.* import kotlin.math.abs +import kotlinx.android.synthetic.main.fragment_mini_player.* open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_player), MusicProgressViewUpdateHelper.Callback, View.OnClickListener { @@ -49,7 +67,6 @@ open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_p actionPrevious.show() actionNext?.show() actionPrevious?.show() - } else { actionNext.visibility = if (PreferenceUtil.isExtraControls) View.VISIBLE else View.GONE actionPrevious.visibility = @@ -126,7 +143,7 @@ open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_p } fun updateProgressBar(paletteColor: Int) { - progressBar.applyColor(paletteColor) + progressBar.applyColor(paletteColor) } class FlingPlayBackController(context: Context) : View.OnTouchListener { @@ -137,7 +154,9 @@ open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_p flingPlayBackController = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onFling( - e1: MotionEvent, e2: MotionEvent, velocityX: Float, + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, velocityY: Float ): Boolean { if (abs(velocityX) > abs(velocityY)) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt index 522a93291..12b514c3d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/NowPlayingScreen.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import androidx.annotation.DrawableRes diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/VolumeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/VolumeFragment.kt index a6783bc01..ed2f88ccd 100755 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/VolumeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/VolumeFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments import android.content.Context @@ -28,7 +42,9 @@ class VolumeFragment : Fragment(), SeekBar.OnSeekBarChangeListener, OnAudioVolum get() = requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_volume, container, false) } @@ -114,13 +130,12 @@ class VolumeFragment : Fragment(), SeekBar.OnSeekBarChangeListener, OnAudioVolum if (PreferenceUtil.isPauseOnZeroVolume) if (MusicPlayerRemote.isPlaying && pauseWhenZeroVolume) MusicPlayerRemote.pauseSong() - } fun setTintableColor(color: Int) { volumeDown.setColorFilter(color, PorterDuff.Mode.SRC_IN) volumeUp.setColorFilter(color, PorterDuff.Mode.SRC_IN) - //TintHelper.setTint(volumeSeekBar, color, false) + // TintHelper.setTint(volumeSeekBar, color, false) volumeSeekBar.applyColor(color) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt index abfea08e9..86f5b2edb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/about/AboutFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.about import android.content.Intent @@ -31,7 +45,6 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener { loadContributors() } - private fun openUrl(url: String) { val i = Intent(Intent.ACTION_VIEW) i.data = Uri.parse(url) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index 85bd13f20..e01f02fbf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.albums import android.app.ActivityOptions @@ -52,6 +66,7 @@ import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import com.google.android.material.transition.MaterialContainerTransform +import java.util.* import kotlinx.android.synthetic.main.fragment_album_content.* import kotlinx.android.synthetic.main.fragment_album_details.* import kotlinx.coroutines.Dispatchers @@ -60,7 +75,6 @@ import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf -import java.util.* class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_details), IAlbumClickListener { @@ -407,4 +421,4 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det companion object { const val TAG_EDITOR_REQUEST = 9002 } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt index f764a98ba..c3f85d727 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.albums import androidx.lifecycle.LiveData @@ -37,7 +51,6 @@ class AlbumDetailsViewModel( } override fun onMediaStoreChanged() { - } override fun onServiceConnected() {} @@ -47,4 +60,4 @@ class AlbumDetailsViewModel( override fun onPlayStateChanged() {} override fun onRepeatModeChanged() {} override fun onShuffleModeChanged() {} -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt index 621aef7e0..7158d7974 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumsFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.albums import android.os.Bundle @@ -22,7 +36,6 @@ import code.name.monkey.retromusic.util.RetroColorUtil import code.name.monkey.retromusic.util.RetroUtil import com.afollestad.materialcab.MaterialCab - class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), IAlbumClickListener, ICabHolder { @@ -95,7 +108,6 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), IArtistClickListener, ICabHolder { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -119,7 +132,6 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment } else R.layout.item_list } - fun setAndSaveLayoutRes(layoutRes: Int) { saveLayoutRes(layoutRes) invalidateAdapter() @@ -51,7 +64,6 @@ abstract class AbsRecyclerViewCustomGridSizeFragment
return gridSize } - fun getSortOrder(): String? { if (sortOrder == null) { sortOrder = loadSortOrder() @@ -59,7 +71,6 @@ abstract class AbsRecyclerViewCustomGridSizeFragment return sortOrder } - fun setAndSaveSortOrder(sortOrder: String) { this.sortOrder = sortOrder saveSortOrder(sortOrder) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt index 4144ef04f..1d06263b5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.base import android.os.Bundle @@ -24,7 +38,6 @@ import kotlinx.android.synthetic.main.fragment_main_recycler.* import me.zhanghai.android.fastscroll.FastScroller import me.zhanghai.android.fastscroll.FastScrollerBuilder - abstract class AbsRecyclerViewFragment, LM : RecyclerView.LayoutManager> : AbsMainActivityFragment(R.layout.fragment_main_recycler), AppBarLayout.OnOffsetChangedListener { @@ -128,7 +141,6 @@ abstract class AbsRecyclerViewFragment, LM : Recycle } } - private fun initLayoutManager() { layoutManager = createLayoutManager() } @@ -206,4 +218,4 @@ abstract class AbsRecyclerViewFragment, LM : Recycle } return super.onOptionsItemSelected(item) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index 43cd9ae58..e0769e8fd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.fragments.folder; +import static code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor; + import android.app.Dialog; import android.content.Context; import android.media.MediaScannerConnection; @@ -32,7 +34,6 @@ import android.webkit.MimeTypeMap; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; @@ -42,23 +43,6 @@ import androidx.loader.content.Loader; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - -import com.afollestad.materialcab.MaterialCab; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; - -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; @@ -83,700 +67,754 @@ import code.name.monkey.retromusic.util.RetroColorUtil; import code.name.monkey.retromusic.util.ThemedFastScroller; import code.name.monkey.retromusic.views.BreadCrumbLayout; import code.name.monkey.retromusic.views.ScrollingViewOnApplyWindowInsetsListener; +import com.afollestad.materialcab.MaterialCab; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; import me.zhanghai.android.fastscroll.FastScroller; +import org.jetbrains.annotations.NotNull; -import static code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor; - -public class FoldersFragment extends AbsMainActivityFragment implements - IMainActivityFragmentCallbacks, +public class FoldersFragment extends AbsMainActivityFragment + implements IMainActivityFragmentCallbacks, ICabHolder, BreadCrumbLayout.SelectionCallback, ICallbacks, LoaderManager.LoaderCallbacks> { - public static final String TAG = FoldersFragment.class.getSimpleName(); - public static final FileFilter AUDIO_FILE_FILTER = file -> !file.isHidden() && (file.isDirectory() || - FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()) || - FileUtil.fileIsMimeType(file, "application/opus", MimeTypeMap.getSingleton()) || - FileUtil.fileIsMimeType(file, "application/ogg", MimeTypeMap.getSingleton())); + public static final String TAG = FoldersFragment.class.getSimpleName(); + public static final FileFilter AUDIO_FILE_FILTER = + file -> + !file.isHidden() + && (file.isDirectory() + || FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()) + || FileUtil.fileIsMimeType(file, "application/opus", MimeTypeMap.getSingleton()) + || FileUtil.fileIsMimeType(file, "application/ogg", MimeTypeMap.getSingleton())); - private static final String CRUMBS = "crumbs"; - private static final int LOADER_ID = 5; - private SongFileAdapter adapter; - private Toolbar toolbar; - private TextView appNameText; - private BreadCrumbLayout breadCrumbs; - private MaterialCab cab; - private View coordinatorLayout; - private View empty; - private TextView emojiText; - private Comparator fileComparator = (lhs, rhs) -> { + private static final String CRUMBS = "crumbs"; + private static final int LOADER_ID = 5; + private SongFileAdapter adapter; + private Toolbar toolbar; + private TextView appNameText; + private BreadCrumbLayout breadCrumbs; + private MaterialCab cab; + private View coordinatorLayout; + private View empty; + private TextView emojiText; + private Comparator fileComparator = + (lhs, rhs) -> { if (lhs.isDirectory() && !rhs.isDirectory()) { - return -1; + return -1; } else if (!lhs.isDirectory() && rhs.isDirectory()) { - return 1; + return 1; } else { - return lhs.getName().compareToIgnoreCase - (rhs.getName()); + return lhs.getName().compareToIgnoreCase(rhs.getName()); } - }; - private RecyclerView recyclerView; + }; + private RecyclerView recyclerView; - public FoldersFragment() { - super(R.layout.fragment_folder); + public FoldersFragment() { + super(R.layout.fragment_folder); + } + + public static File getDefaultStartDirectory() { + File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); + File startFolder; + if (musicDir.exists() && musicDir.isDirectory()) { + startFolder = musicDir; + } else { + File externalStorage = Environment.getExternalStorageDirectory(); + if (externalStorage.exists() && externalStorage.isDirectory()) { + startFolder = externalStorage; + } else { + startFolder = new File("/"); // root + } } + return startFolder; + } - public static File getDefaultStartDirectory() { - File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); - File startFolder; - if (musicDir.exists() && musicDir.isDirectory()) { - startFolder = musicDir; - } else { - File externalStorage = Environment.getExternalStorageDirectory(); - if (externalStorage.exists() && externalStorage.isDirectory()) { - startFolder = externalStorage; - } else { - startFolder = new File("/"); // root + private static File tryGetCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + e.printStackTrace(); + return file; + } + } + + @NonNull + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_folder, container, false); + initViews(view); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); + getMainActivity().setSupportActionBar(toolbar); + getMainActivity().getSupportActionBar().setTitle(null); + setStatusBarColorAuto(view); + setUpAppbarColor(); + setUpBreadCrumbs(); + setUpRecyclerView(); + setUpAdapter(); + setUpTitle(); + } + + private void setUpTitle() { + toolbar.setNavigationOnClickListener( + v -> Navigation.findNavController(v).navigate(R.id.searchFragment, null, getNavOptions())); + int color = ThemeStore.Companion.accentColor(requireContext()); + String hexColor = String.format("#%06X", 0xFFFFFF & color); + Spanned appName = + HtmlCompat.fromHtml( + "Retro Music", + HtmlCompat.FROM_HTML_MODE_COMPACT); + appNameText.setText(appName); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + if (savedInstanceState == null) { + setCrumb( + new BreadCrumbLayout.Crumb( + FileUtil.safeGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), + true); + } else { + breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS)); + LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this); + } + } + + @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (breadCrumbs != null) { + outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper()); + } + } + + @Override + public boolean handleBackPress() { + if (cab != null && cab.isActive()) { + cab.finish(); + return true; + } + if (breadCrumbs != null && breadCrumbs.popHistory()) { + setCrumb(breadCrumbs.lastHistory(), false); + return true; + } + return false; + } + + @NonNull + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new AsyncFileLoader(this); + } + + @Override + public void onCrumbSelection(BreadCrumbLayout.Crumb crumb, int index) { + setCrumb(crumb, true); + } + + @Override + public void onFileMenuClicked(final File file, @NotNull View view) { + PopupMenu popupMenu = new PopupMenu(getActivity(), view); + if (file.isDirectory()) { + popupMenu.inflate(R.menu.menu_item_directory); + popupMenu.setOnMenuItemClickListener( + item -> { + final int itemId = item.getItemId(); + switch (itemId) { + case R.id.action_play_next: + case R.id.action_add_to_current_playing: + case R.id.action_add_to_playlist: + case R.id.action_delete_from_device: + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> { + if (!songs.isEmpty()) { + SongsMenuHelper.INSTANCE.handleMenuClick( + requireActivity(), songs, itemId); + } + }) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file), AUDIO_FILE_FILTER, getFileComparator())); + return true; + case R.id.action_set_as_start_directory: + PreferenceUtil.INSTANCE.setStartDirectory(file); + Toast.makeText( + getActivity(), + String.format(getString(R.string.new_start_directory), file.getPath()), + Toast.LENGTH_SHORT) + .show(); + return true; + case R.id.action_scan: + new ListPathsAsyncTask(getActivity(), this::scanPaths) + .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); + return true; } - } - return startFolder; + return false; + }); + } else { + popupMenu.inflate(R.menu.menu_item_file); + popupMenu.setOnMenuItemClickListener( + item -> { + final int itemId = item.getItemId(); + switch (itemId) { + case R.id.action_play_next: + case R.id.action_add_to_current_playing: + case R.id.action_add_to_playlist: + case R.id.action_go_to_album: + case R.id.action_go_to_artist: + case R.id.action_share: + case R.id.action_tag_editor: + case R.id.action_details: + case R.id.action_set_as_ringtone: + case R.id.action_delete_from_device: + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> + SongMenuHelper.INSTANCE.handleMenuClick( + requireActivity(), songs.get(0), itemId)) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file), AUDIO_FILE_FILTER, getFileComparator())); + return true; + case R.id.action_scan: + new ListPathsAsyncTask(getActivity(), this::scanPaths) + .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); + return true; + } + return false; + }); } + popupMenu.show(); + } - private static File tryGetCanonicalFile(File file) { - try { - return file.getCanonicalFile(); - } catch (IOException e) { - e.printStackTrace(); - return file; - } - } - - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_folder, container, false); - initViews(view); - return view; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); - getMainActivity().setSupportActionBar(toolbar); - getMainActivity().getSupportActionBar().setTitle(null); - setStatusBarColorAuto(view); - setUpAppbarColor(); - setUpBreadCrumbs(); - setUpRecyclerView(); - setUpAdapter(); - setUpTitle(); - } - - private void setUpTitle() { - toolbar.setNavigationOnClickListener(v -> - Navigation.findNavController(v).navigate(R.id.searchFragment, null, getNavOptions()) - ); - int color = ThemeStore.Companion.accentColor(requireContext()); - String hexColor = String.format("#%06X", 0xFFFFFF & color); - Spanned appName = HtmlCompat.fromHtml( - "Retro Music", - HtmlCompat.FROM_HTML_MODE_COMPACT - ); - appNameText.setText(appName); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - if (savedInstanceState == null) { - setCrumb(new BreadCrumbLayout.Crumb(FileUtil.safeGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), true); - } else { - breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS)); - LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this); - } - } - - @Override - public void onPause() { - super.onPause(); - saveScrollPosition(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (breadCrumbs != null) { - outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper()); - } - } - - @Override - public boolean handleBackPress() { - if (cab != null && cab.isActive()) { - cab.finish(); - return true; - } - if (breadCrumbs != null && breadCrumbs.popHistory()) { - setCrumb(breadCrumbs.lastHistory(), false); - return true; - } - return false; - } - - @NonNull - @Override - public Loader> onCreateLoader(int id, Bundle args) { - return new AsyncFileLoader(this); - } - - @Override - public void onCrumbSelection(BreadCrumbLayout.Crumb crumb, int index) { - setCrumb(crumb, true); - } - - @Override - public void onFileMenuClicked(final File file, @NotNull View view) { - PopupMenu popupMenu = new PopupMenu(getActivity(), view); - if (file.isDirectory()) { - popupMenu.inflate(R.menu.menu_item_directory); - popupMenu.setOnMenuItemClickListener(item -> { - final int itemId = item.getItemId(); - switch (itemId) { - case R.id.action_play_next: - case R.id.action_add_to_current_playing: - case R.id.action_add_to_playlist: - case R.id.action_delete_from_device: - new ListSongsAsyncTask(getActivity(), null, (songs, extra) -> { - if (!songs.isEmpty()) { - SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId); - } - }).execute(new ListSongsAsyncTask.LoadingInfo(toList(file), AUDIO_FILE_FILTER, - getFileComparator())); - return true; - case R.id.action_set_as_start_directory: - PreferenceUtil.INSTANCE.setStartDirectory(file); - Toast.makeText(getActivity(), - String.format(getString(R.string.new_start_directory), file.getPath()), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.action_scan: - new ListPathsAsyncTask(getActivity(), this::scanPaths) - .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); - return true; - } - return false; - }); - } else { - popupMenu.inflate(R.menu.menu_item_file); - popupMenu.setOnMenuItemClickListener(item -> { - final int itemId = item.getItemId(); - switch (itemId) { - case R.id.action_play_next: - case R.id.action_add_to_current_playing: - case R.id.action_add_to_playlist: - case R.id.action_go_to_album: - case R.id.action_go_to_artist: - case R.id.action_share: - case R.id.action_tag_editor: - case R.id.action_details: - case R.id.action_set_as_ringtone: - case R.id.action_delete_from_device: - new ListSongsAsyncTask(getActivity(), null, - (songs, extra) -> SongMenuHelper.INSTANCE.handleMenuClick(requireActivity(), - songs.get(0), itemId)) - .execute(new ListSongsAsyncTask.LoadingInfo(toList(file), AUDIO_FILE_FILTER, - getFileComparator())); - return true; - case R.id.action_scan: - new ListPathsAsyncTask(getActivity(), this::scanPaths) - .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); - return true; - } - return false; - }); - } - popupMenu.show(); - } - - @Override - public void onFileSelected(@NotNull File file) { - file = tryGetCanonicalFile(file); // important as we compare the path value later - if (file.isDirectory()) { - setCrumb(new BreadCrumbLayout.Crumb(file), true); - } else { - FileFilter fileFilter = pathname -> !pathname.isDirectory() && AUDIO_FILE_FILTER - .accept(pathname); - new ListSongsAsyncTask(getActivity(), file, (songs, extra) -> { + @Override + public void onFileSelected(@NotNull File file) { + file = tryGetCanonicalFile(file); // important as we compare the path value later + if (file.isDirectory()) { + setCrumb(new BreadCrumbLayout.Crumb(file), true); + } else { + FileFilter fileFilter = + pathname -> !pathname.isDirectory() && AUDIO_FILE_FILTER.accept(pathname); + new ListSongsAsyncTask( + getActivity(), + file, + (songs, extra) -> { File file1 = (File) extra; int startIndex = -1; for (int i = 0; i < songs.size(); i++) { - if (file1.getPath().equals(songs.get(i).getData())) { // path is already canonical here - startIndex = i; - break; - } + if (file1 + .getPath() + .equals(songs.get(i).getData())) { // path is already canonical here + startIndex = i; + break; + } } if (startIndex > -1) { - MusicPlayerRemote.openQueue(songs, startIndex, true); + MusicPlayerRemote.openQueue(songs, startIndex, true); } else { - final File finalFile = file1; - Snackbar.make(coordinatorLayout, Html.fromHtml( - String.format(getString(R.string.not_listed_in_media_store), file1.getName())), - Snackbar.LENGTH_LONG) - .setAction(R.string.action_scan, - v -> new ListPathsAsyncTask(requireActivity(), this::scanPaths) - .execute( - new ListPathsAsyncTask.LoadingInfo(finalFile, AUDIO_FILE_FILTER))) - .setActionTextColor(ThemeStore.Companion.accentColor(requireActivity())) - .show(); + final File finalFile = file1; + Snackbar.make( + coordinatorLayout, + Html.fromHtml( + String.format( + getString(R.string.not_listed_in_media_store), file1.getName())), + Snackbar.LENGTH_LONG) + .setAction( + R.string.action_scan, + v -> + new ListPathsAsyncTask(requireActivity(), this::scanPaths) + .execute( + new ListPathsAsyncTask.LoadingInfo( + finalFile, AUDIO_FILE_FILTER))) + .setActionTextColor(ThemeStore.Companion.accentColor(requireActivity())) + .show(); } - }).execute(new ListSongsAsyncTask.LoadingInfo(toList(file.getParentFile()), fileFilter, - getFileComparator())); - } + }) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file.getParentFile()), fileFilter, getFileComparator())); } + } - @Override - public void onLoadFinished(@NonNull Loader> loader, List data) { - updateAdapter(data); - } + @Override + public void onLoadFinished(@NonNull Loader> loader, List data) { + updateAdapter(data); + } - @Override - public void onLoaderReset(@NonNull Loader> loader) { - updateAdapter(new LinkedList()); - } + @Override + public void onLoaderReset(@NonNull Loader> loader) { + updateAdapter(new LinkedList()); + } - @Override - public void onMultipleItemAction(MenuItem item, @NotNull ArrayList files) { - final int itemId = item.getItemId(); - new ListSongsAsyncTask(getActivity(), null, - (songs, extra) -> SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId)) - .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); - } + @Override + public void onMultipleItemAction(MenuItem item, @NotNull ArrayList files) { + final int itemId = item.getItemId(); + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> + SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId)) + .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); + } - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar); - } + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar); + } - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.add(0, R.id.action_scan, 0, R.string.scan_media).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - menu.removeItem(R.id.action_grid_size); - menu.removeItem(R.id.action_layout_type); - menu.removeItem(R.id.action_sort_order); - ToolbarContentTintHelper.handleOnCreateOptionsMenu( - requireContext(), - toolbar, - menu, - getToolbarBackgroundColor(toolbar) - ); - } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.add(0, R.id.action_scan, 0, R.string.scan_media) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.removeItem(R.id.action_grid_size); + menu.removeItem(R.id.action_layout_type); + menu.removeItem(R.id.action_sort_order); + ToolbarContentTintHelper.handleOnCreateOptionsMenu( + requireContext(), toolbar, menu, getToolbarBackgroundColor(toolbar)); + } - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.action_go_to_start_directory: - setCrumb(new BreadCrumbLayout.Crumb( - tryGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), true); - return true; - case R.id.action_scan: - BreadCrumbLayout.Crumb crumb = getActiveCrumb(); - if (crumb != null) { - //noinspection Convert2MethodRef - new ListPathsAsyncTask(getActivity(), paths -> scanPaths(paths)) - .execute(new ListPathsAsyncTask.LoadingInfo(crumb.getFile(), - AUDIO_FILE_FILTER)); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onQueueChanged() { - super.onQueueChanged(); - checkForPadding(); - } - - @Override - public void onServiceConnected() { - super.onServiceConnected(); - checkForPadding(); - } - - @NonNull - @Override - public MaterialCab openCab(int menuRes, @NotNull MaterialCab.Callback callback) { - if (cab != null && cab.isActive()) { - cab.finish(); - } - cab = new MaterialCab(getMainActivity(), R.id.cab_stub) - .setMenu(menuRes) - .setCloseDrawableRes(R.drawable.ic_close) - .setBackgroundColor(RetroColorUtil.shiftBackgroundColorForLightText( - ATHUtil.INSTANCE.resolveColor(requireContext(), R.attr.colorSurface))) - .start(callback); - return cab; - } - - private void checkForPadding() { - final int count = adapter.getItemCount(); - final MarginLayoutParams params = (MarginLayoutParams) coordinatorLayout.getLayoutParams(); - params.bottomMargin = count > 0 && !MusicPlayerRemote.getPlayingQueue().isEmpty() ? DensityUtil - .dip2px(requireContext(), 104f) : DensityUtil.dip2px(requireContext(), 54f); - } - - private void checkIsEmpty() { - emojiText.setText(getEmojiByUnicode(0x1F631)); - if (empty != null) { - empty.setVisibility(adapter == null || adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - } - - @Nullable - private BreadCrumbLayout.Crumb getActiveCrumb() { - return breadCrumbs != null && breadCrumbs.size() > 0 ? breadCrumbs - .getCrumb(breadCrumbs.getActiveIndex()) : null; - } - - private String getEmojiByUnicode(int unicode) { - return new String(Character.toChars(unicode)); - } - - private Comparator getFileComparator() { - return fileComparator; - } - - private void initViews(View view) { - coordinatorLayout = view.findViewById(R.id.coordinatorLayout); - recyclerView = view.findViewById(R.id.recyclerView); - breadCrumbs = view.findViewById(R.id.breadCrumbs); - empty = view.findViewById(android.R.id.empty); - emojiText = view.findViewById(R.id.emptyEmoji); - toolbar = view.findViewById(R.id.toolbar); - appNameText = view.findViewById(R.id.appNameText); - } - - private void saveScrollPosition() { + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_go_to_start_directory: + setCrumb( + new BreadCrumbLayout.Crumb( + tryGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), + true); + return true; + case R.id.action_scan: BreadCrumbLayout.Crumb crumb = getActiveCrumb(); if (crumb != null) { - crumb.setScrollPosition( - ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition()); + //noinspection Convert2MethodRef + new ListPathsAsyncTask(getActivity(), paths -> scanPaths(paths)) + .execute(new ListPathsAsyncTask.LoadingInfo(crumb.getFile(), AUDIO_FILE_FILTER)); } + return true; } + return super.onOptionsItemSelected(item); + } - private void scanPaths(@Nullable String[] toBeScanned) { - if (getActivity() == null) { - return; - } - if (toBeScanned == null || toBeScanned.length < 1) { - Toast.makeText(getActivity(), R.string.nothing_to_scan, Toast.LENGTH_SHORT).show(); - } else { - MediaScannerConnection.scanFile(getActivity().getApplicationContext(), toBeScanned, null, - new UpdateToastMediaScannerCompletionListener(getActivity(), toBeScanned)); - } + @Override + public void onQueueChanged() { + super.onQueueChanged(); + checkForPadding(); + } + + @Override + public void onServiceConnected() { + super.onServiceConnected(); + checkForPadding(); + } + + @NonNull + @Override + public MaterialCab openCab(int menuRes, @NotNull MaterialCab.Callback callback) { + if (cab != null && cab.isActive()) { + cab.finish(); } + cab = + new MaterialCab(getMainActivity(), R.id.cab_stub) + .setMenu(menuRes) + .setCloseDrawableRes(R.drawable.ic_close) + .setBackgroundColor( + RetroColorUtil.shiftBackgroundColorForLightText( + ATHUtil.INSTANCE.resolveColor(requireContext(), R.attr.colorSurface))) + .start(callback); + return cab; + } - private void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) { - if (crumb == null) { - return; - } - saveScrollPosition(); - breadCrumbs.setActiveOrAdd(crumb, false); - if (addToHistory) { - breadCrumbs.addHistory(crumb); - } - LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this); + private void checkForPadding() { + final int count = adapter.getItemCount(); + final MarginLayoutParams params = (MarginLayoutParams) coordinatorLayout.getLayoutParams(); + params.bottomMargin = + count > 0 && !MusicPlayerRemote.getPlayingQueue().isEmpty() + ? DensityUtil.dip2px(requireContext(), 104f) + : DensityUtil.dip2px(requireContext(), 54f); + } + + private void checkIsEmpty() { + emojiText.setText(getEmojiByUnicode(0x1F631)); + if (empty != null) { + empty.setVisibility( + adapter == null || adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); } + } - private void setUpAdapter() { - adapter = new SongFileAdapter(getMainActivity(), new LinkedList<>(), R.layout.item_list, - this, this); - adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - super.onChanged(); - checkIsEmpty(); - checkForPadding(); - } + @Nullable + private BreadCrumbLayout.Crumb getActiveCrumb() { + return breadCrumbs != null && breadCrumbs.size() > 0 + ? breadCrumbs.getCrumb(breadCrumbs.getActiveIndex()) + : null; + } + + private String getEmojiByUnicode(int unicode) { + return new String(Character.toChars(unicode)); + } + + private Comparator getFileComparator() { + return fileComparator; + } + + private void initViews(View view) { + coordinatorLayout = view.findViewById(R.id.coordinatorLayout); + recyclerView = view.findViewById(R.id.recyclerView); + breadCrumbs = view.findViewById(R.id.breadCrumbs); + empty = view.findViewById(android.R.id.empty); + emojiText = view.findViewById(R.id.emptyEmoji); + toolbar = view.findViewById(R.id.toolbar); + appNameText = view.findViewById(R.id.appNameText); + } + + private void saveScrollPosition() { + BreadCrumbLayout.Crumb crumb = getActiveCrumb(); + if (crumb != null) { + crumb.setScrollPosition( + ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition()); + } + } + + private void scanPaths(@Nullable String[] toBeScanned) { + if (getActivity() == null) { + return; + } + if (toBeScanned == null || toBeScanned.length < 1) { + Toast.makeText(getActivity(), R.string.nothing_to_scan, Toast.LENGTH_SHORT).show(); + } else { + MediaScannerConnection.scanFile( + getActivity().getApplicationContext(), + toBeScanned, + null, + new UpdateToastMediaScannerCompletionListener(getActivity(), toBeScanned)); + } + } + + private void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) { + if (crumb == null) { + return; + } + saveScrollPosition(); + breadCrumbs.setActiveOrAdd(crumb, false); + if (addToHistory) { + breadCrumbs.addHistory(crumb); + } + LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this); + } + + private void setUpAdapter() { + adapter = + new SongFileAdapter(getMainActivity(), new LinkedList<>(), R.layout.item_list, this, this); + adapter.registerAdapterDataObserver( + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + checkIsEmpty(); + checkForPadding(); + } }); - recyclerView.setAdapter(adapter); - checkIsEmpty(); + recyclerView.setAdapter(adapter); + checkIsEmpty(); + } + + private void setUpAppbarColor() { + breadCrumbs.setActivatedContentColor( + ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorPrimary)); + breadCrumbs.setDeactivatedContentColor( + ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorSecondary)); + } + + private void setUpBreadCrumbs() { + breadCrumbs.setCallback(this); + } + + private void setUpRecyclerView() { + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + FastScroller fastScroller = ThemedFastScroller.INSTANCE.create(recyclerView); + recyclerView.setOnApplyWindowInsetsListener( + new ScrollingViewOnApplyWindowInsetsListener(recyclerView, fastScroller)); + } + + private ArrayList toList(File file) { + ArrayList files = new ArrayList<>(1); + files.add(file); + return files; + } + + private void updateAdapter(@NonNull List files) { + adapter.swapDataSet(files); + BreadCrumbLayout.Crumb crumb = getActiveCrumb(); + if (crumb != null && recyclerView != null) { + ((LinearLayoutManager) recyclerView.getLayoutManager()) + .scrollToPositionWithOffset(crumb.getScrollPosition(), 0); + } + } + + public static class ListPathsAsyncTask + extends ListingFilesDialogAsyncTask { + + private WeakReference onPathsListedCallbackWeakReference; + + public ListPathsAsyncTask(Context context, OnPathsListedCallback callback) { + super(context); + onPathsListedCallbackWeakReference = new WeakReference<>(callback); } - private void setUpAppbarColor() { - breadCrumbs.setActivatedContentColor( - ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorPrimary)); - breadCrumbs.setDeactivatedContentColor( - ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorSecondary)); + @Override + protected String[] doInBackground(LoadingInfo... params) { + try { + if (isCancelled() || checkCallbackReference() == null) { + return null; + } + + LoadingInfo info = params[0]; + + final String[] paths; + + if (info.file.isDirectory()) { + List files = FileUtil.listFilesDeep(info.file, info.fileFilter); + + if (isCancelled() || checkCallbackReference() == null) { + return null; + } + + paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) { + File f = files.get(i); + paths[i] = FileUtil.safeGetCanonicalPath(f); + + if (isCancelled() || checkCallbackReference() == null) { + return null; + } + } + } else { + paths = new String[1]; + paths[0] = info.file.getPath(); + } + + return paths; + } catch (Exception e) { + e.printStackTrace(); + cancel(false); + return null; + } } - private void setUpBreadCrumbs() { - breadCrumbs.setCallback(this); + @Override + protected void onPostExecute(String[] paths) { + super.onPostExecute(paths); + OnPathsListedCallback callback = checkCallbackReference(); + if (callback != null && paths != null) { + callback.onPathsListed(paths); + } } - private void setUpRecyclerView() { - recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - FastScroller fastScroller = ThemedFastScroller.INSTANCE.create(recyclerView); - recyclerView.setOnApplyWindowInsetsListener( - new ScrollingViewOnApplyWindowInsetsListener(recyclerView, fastScroller)); + @Override + protected void onPreExecute() { + super.onPreExecute(); + checkCallbackReference(); } - private ArrayList toList(File file) { - ArrayList files = new ArrayList<>(1); - files.add(file); + private OnPathsListedCallback checkCallbackReference() { + OnPathsListedCallback callback = onPathsListedCallbackWeakReference.get(); + if (callback == null) { + cancel(false); + } + return callback; + } + + public interface OnPathsListedCallback { + + void onPathsListed(@NonNull String[] paths); + } + + public static class LoadingInfo { + + public final File file; + + final FileFilter fileFilter; + + public LoadingInfo(File file, FileFilter fileFilter) { + this.file = file; + this.fileFilter = fileFilter; + } + } + } + + private static class AsyncFileLoader extends WrappedAsyncTaskLoader> { + + private WeakReference fragmentWeakReference; + + AsyncFileLoader(FoldersFragment foldersFragment) { + super(foldersFragment.requireActivity()); + fragmentWeakReference = new WeakReference<>(foldersFragment); + } + + @Override + public List loadInBackground() { + FoldersFragment foldersFragment = fragmentWeakReference.get(); + File directory = null; + if (foldersFragment != null) { + BreadCrumbLayout.Crumb crumb = foldersFragment.getActiveCrumb(); + if (crumb != null) { + directory = crumb.getFile(); + } + } + if (directory != null) { + List files = FileUtil.listFiles(directory, AUDIO_FILE_FILTER); + Collections.sort(files, foldersFragment.getFileComparator()); return files; + } else { + return new LinkedList<>(); + } + } + } + + private static class ListSongsAsyncTask + extends ListingFilesDialogAsyncTask> { + + private final Object extra; + private WeakReference callbackWeakReference; + private WeakReference contextWeakReference; + + ListSongsAsyncTask(Context context, Object extra, OnSongsListedCallback callback) { + super(context); + this.extra = extra; + contextWeakReference = new WeakReference<>(context); + callbackWeakReference = new WeakReference<>(callback); } - private void updateAdapter(@NonNull List files) { - adapter.swapDataSet(files); - BreadCrumbLayout.Crumb crumb = getActiveCrumb(); - if (crumb != null && recyclerView != null) { - ((LinearLayoutManager) recyclerView.getLayoutManager()) - .scrollToPositionWithOffset(crumb.getScrollPosition(), 0); + @Override + protected List doInBackground(LoadingInfo... params) { + try { + LoadingInfo info = params[0]; + List files = FileUtil.listFilesDeep(info.files, info.fileFilter); + + if (isCancelled() || checkContextReference() == null || checkCallbackReference() == null) { + return null; } + + Collections.sort(files, info.fileComparator); + + Context context = checkContextReference(); + if (isCancelled() || context == null || checkCallbackReference() == null) { + return null; + } + + return FileUtil.matchFilesWithMediaStore(context, files); + } catch (Exception e) { + e.printStackTrace(); + cancel(false); + return null; + } } - public static class ListPathsAsyncTask extends - ListingFilesDialogAsyncTask { - - private WeakReference onPathsListedCallbackWeakReference; - - public ListPathsAsyncTask(Context context, OnPathsListedCallback callback) { - super(context); - onPathsListedCallbackWeakReference = new WeakReference<>(callback); - } - - @Override - protected String[] doInBackground(LoadingInfo... params) { - try { - if (isCancelled() || checkCallbackReference() == null) { - return null; - } - - LoadingInfo info = params[0]; - - final String[] paths; - - if (info.file.isDirectory()) { - List files = FileUtil.listFilesDeep(info.file, info.fileFilter); - - if (isCancelled() || checkCallbackReference() == null) { - return null; - } - - paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) { - File f = files.get(i); - paths[i] = FileUtil.safeGetCanonicalPath(f); - - if (isCancelled() || checkCallbackReference() == null) { - return null; - } - } - } else { - paths = new String[1]; - paths[0] = info.file.getPath(); - } - - return paths; - } catch (Exception e) { - e.printStackTrace(); - cancel(false); - return null; - } - } - - @Override - protected void onPostExecute(String[] paths) { - super.onPostExecute(paths); - OnPathsListedCallback callback = checkCallbackReference(); - if (callback != null && paths != null) { - callback.onPathsListed(paths); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - checkCallbackReference(); - } - - private OnPathsListedCallback checkCallbackReference() { - OnPathsListedCallback callback = onPathsListedCallbackWeakReference.get(); - if (callback == null) { - cancel(false); - } - return callback; - } - - public interface OnPathsListedCallback { - - void onPathsListed(@NonNull String[] paths); - } - - public static class LoadingInfo { - - public final File file; - - final FileFilter fileFilter; - - public LoadingInfo(File file, FileFilter fileFilter) { - this.file = file; - this.fileFilter = fileFilter; - } - } + @Override + protected void onPostExecute(List songs) { + super.onPostExecute(songs); + OnSongsListedCallback callback = checkCallbackReference(); + if (songs != null && callback != null) { + callback.onSongsListed(songs, extra); + } } - private static class AsyncFileLoader extends WrappedAsyncTaskLoader> { - - private WeakReference fragmentWeakReference; - - AsyncFileLoader(FoldersFragment foldersFragment) { - super(foldersFragment.requireActivity()); - fragmentWeakReference = new WeakReference<>(foldersFragment); - } - - @Override - public List loadInBackground() { - FoldersFragment foldersFragment = fragmentWeakReference.get(); - File directory = null; - if (foldersFragment != null) { - BreadCrumbLayout.Crumb crumb = foldersFragment.getActiveCrumb(); - if (crumb != null) { - directory = crumb.getFile(); - } - } - if (directory != null) { - List files = FileUtil.listFiles(directory, AUDIO_FILE_FILTER); - Collections.sort(files, foldersFragment.getFileComparator()); - return files; - } else { - return new LinkedList<>(); - } - } + @Override + protected void onPreExecute() { + super.onPreExecute(); + checkCallbackReference(); + checkContextReference(); } - private static class ListSongsAsyncTask - extends ListingFilesDialogAsyncTask> { - - private final Object extra; - private WeakReference callbackWeakReference; - private WeakReference contextWeakReference; - - ListSongsAsyncTask(Context context, Object extra, OnSongsListedCallback callback) { - super(context); - this.extra = extra; - contextWeakReference = new WeakReference<>(context); - callbackWeakReference = new WeakReference<>(callback); - } - - @Override - protected List doInBackground(LoadingInfo... params) { - try { - LoadingInfo info = params[0]; - List files = FileUtil.listFilesDeep(info.files, info.fileFilter); - - if (isCancelled() || checkContextReference() == null - || checkCallbackReference() == null) { - return null; - } - - Collections.sort(files, info.fileComparator); - - Context context = checkContextReference(); - if (isCancelled() || context == null || checkCallbackReference() == null) { - return null; - } - - return FileUtil.matchFilesWithMediaStore(context, files); - } catch (Exception e) { - e.printStackTrace(); - cancel(false); - return null; - } - } - - @Override - protected void onPostExecute(List songs) { - super.onPostExecute(songs); - OnSongsListedCallback callback = checkCallbackReference(); - if (songs != null && callback != null) { - callback.onSongsListed(songs, extra); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - checkCallbackReference(); - checkContextReference(); - } - - private OnSongsListedCallback checkCallbackReference() { - OnSongsListedCallback callback = callbackWeakReference.get(); - if (callback == null) { - cancel(false); - } - return callback; - } - - private Context checkContextReference() { - Context context = contextWeakReference.get(); - if (context == null) { - cancel(false); - } - return context; - } - - public interface OnSongsListedCallback { - - void onSongsListed(@NonNull List songs, Object extra); - } - - static class LoadingInfo { - - final Comparator fileComparator; - - final FileFilter fileFilter; - - final List files; - - LoadingInfo(@NonNull List files, @NonNull FileFilter fileFilter, - @NonNull Comparator fileComparator) { - this.fileComparator = fileComparator; - this.fileFilter = fileFilter; - this.files = files; - } - } + private OnSongsListedCallback checkCallbackReference() { + OnSongsListedCallback callback = callbackWeakReference.get(); + if (callback == null) { + cancel(false); + } + return callback; } - private static abstract class ListingFilesDialogAsyncTask extends - DialogAsyncTask { - - ListingFilesDialogAsyncTask(Context context) { - super(context); - } - - public ListingFilesDialogAsyncTask(Context context, int showDelay) { - super(context, showDelay); - } - - @Override - protected Dialog createDialog(@NonNull Context context) { - return new MaterialAlertDialogBuilder(context) - .setTitle(R.string.listing_files) - .setCancelable(false) - .setView(R.layout.loading) - .setOnCancelListener(dialog -> cancel(false)) - .setOnDismissListener(dialog -> cancel(false)) - .create(); - } + private Context checkContextReference() { + Context context = contextWeakReference.get(); + if (context == null) { + cancel(false); + } + return context; } + + public interface OnSongsListedCallback { + + void onSongsListed(@NonNull List songs, Object extra); + } + + static class LoadingInfo { + + final Comparator fileComparator; + + final FileFilter fileFilter; + + final List files; + + LoadingInfo( + @NonNull List files, + @NonNull FileFilter fileFilter, + @NonNull Comparator fileComparator) { + this.fileComparator = fileComparator; + this.fileFilter = fileFilter; + this.files = files; + } + } + } + + private abstract static class ListingFilesDialogAsyncTask + extends DialogAsyncTask { + + ListingFilesDialogAsyncTask(Context context) { + super(context); + } + + public ListingFilesDialogAsyncTask(Context context, int showDelay) { + super(context, showDelay); + } + + @Override + protected Dialog createDialog(@NonNull Context context) { + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.listing_files) + .setCancelable(false) + .setView(R.layout.loading) + .setOnCancelListener(dialog -> cancel(false)) + .setOnDismissListener(dialog -> cancel(false)) + .create(); + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt index cca506d1a..c2190eb59 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.genres import android.os.Bundle @@ -17,10 +31,10 @@ import code.name.monkey.retromusic.helper.menu.GenreMenuHelper import code.name.monkey.retromusic.model.Genre import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.state.NowPlayingPanelState +import java.util.* import kotlinx.android.synthetic.main.fragment_playlist_detail.* import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf -import java.util.* class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_detail) { private val arguments by navArgs() @@ -89,4 +103,4 @@ class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_ override fun onOptionsItemSelected(item: MenuItem): Boolean { return GenreMenuHelper.handleMenuClick(requireActivity(), genre, item) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt index ceb419d83..43f941df8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsViewModel.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.genres import androidx.lifecycle.LiveData @@ -46,4 +60,4 @@ class GenreDetailsViewModel( override fun onPlayStateChanged() {} override fun onRepeatModeChanged() {} override fun onShuffleModeChanged() {} -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt index 3574c8a9a..e7c5041aa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenresFragment.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.fragments.genres import android.os.Bundle @@ -33,7 +33,6 @@ class GenresFragment : AbsRecyclerViewFragment> = realRepository.playlistSongs(playlist.playlistEntity.playListId) - override fun onMediaStoreChanged() { /*if (playlist !is AbsCustomPlaylist) { // Playlist deleted diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt index 9744cb8e9..275929357 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.fragments.playlists import android.os.Bundle diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/queue/PlayingQueueFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/queue/PlayingQueueFragment.kt index 6376420c9..0af0c72c4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/queue/PlayingQueueFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/queue/PlayingQueueFragment.kt @@ -1,15 +1,16 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ package code.name.monkey.retromusic.fragments.queue @@ -139,4 +140,4 @@ class PlayingQueueFragment : AbsRecyclerViewFragment(), ICabHolder { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/AlbumGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/AlbumGlideRequest.java index 82e65da20..2158addc7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/AlbumGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/AlbumGlideRequest.java @@ -2,9 +2,14 @@ package code.name.monkey.retromusic.glide; import android.content.Context; import android.graphics.Bitmap; - import androidx.annotation.NonNull; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.glide.audiocover.AudioFileCover; +import code.name.monkey.retromusic.glide.palette.BitmapPaletteTranscoder; +import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper; +import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.util.MusicUtil; +import code.name.monkey.retromusic.util.PreferenceUtil; import com.bumptech.glide.BitmapRequestBuilder; import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.DrawableTypeRequest; @@ -14,120 +19,112 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.signature.MediaStoreSignature; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.glide.audiocover.AudioFileCover; -import code.name.monkey.retromusic.glide.palette.BitmapPaletteTranscoder; -import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper; -import code.name.monkey.retromusic.model.Song; -import code.name.monkey.retromusic.util.MusicUtil; -import code.name.monkey.retromusic.util.PreferenceUtil; - public class AlbumGlideRequest { - private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; - private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_album_art; - private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; + private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; + private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_album_art; + private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; - @NonNull - private static DrawableTypeRequest createBaseRequest(@NonNull RequestManager requestManager, - @NonNull Song song, - boolean ignoreMediaStore) { - if (ignoreMediaStore) { - return requestManager.load(new AudioFileCover(song.getData())); - } else { - return requestManager.loadFromMediaStore(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(song.getAlbumId())); - } + @NonNull + private static DrawableTypeRequest createBaseRequest( + @NonNull RequestManager requestManager, @NonNull Song song, boolean ignoreMediaStore) { + if (ignoreMediaStore) { + return requestManager.load(new AudioFileCover(song.getData())); + } else { + return requestManager.loadFromMediaStore( + MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(song.getAlbumId())); + } + } + + @NonNull + private static Key createSignature(@NonNull Song song) { + return new MediaStoreSignature("", song.getDateModified(), 0); + } + + public static class Builder { + final RequestManager requestManager; + final Song song; + boolean ignoreMediaStore; + + private Builder(@NonNull RequestManager requestManager, Song song) { + this.requestManager = requestManager; + this.song = song; } @NonNull - private static Key createSignature(@NonNull Song song) { - return new MediaStoreSignature("", song.getDateModified(), 0); + public static Builder from(@NonNull RequestManager requestManager, Song song) { + return new Builder(requestManager, song); } - public static class Builder { - final RequestManager requestManager; - final Song song; - boolean ignoreMediaStore; - - private Builder(@NonNull RequestManager requestManager, Song song) { - this.requestManager = requestManager; - this.song = song; - } - - @NonNull - public static Builder from(@NonNull RequestManager requestManager, Song song) { - return new Builder(requestManager, song); - } - - @NonNull - public PaletteBuilder generatePalette(@NonNull Context context) { - return new PaletteBuilder(this, context); - } - - @NonNull - public BitmapBuilder asBitmap() { - return new BitmapBuilder(this); - } - - @NonNull - public Builder checkIgnoreMediaStore() { - return ignoreMediaStore(PreferenceUtil.INSTANCE.isIgnoreMediaStoreArtwork()); - } - - @NonNull - public Builder ignoreMediaStore(boolean ignoreMediaStore) { - this.ignoreMediaStore = ignoreMediaStore; - return this; - } - - @NonNull - public DrawableRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(requestManager, song, ignoreMediaStore) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(song)); - } + @NonNull + public PaletteBuilder generatePalette(@NonNull Context context) { + return new PaletteBuilder(this, context); } - public static class BitmapBuilder { - private final Builder builder; - - BitmapBuilder(Builder builder) { - this.builder = builder; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) - .asBitmap() - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .dontTransform() - .signature(createSignature(builder.song)); - } + @NonNull + public BitmapBuilder asBitmap() { + return new BitmapBuilder(this); } - public static class PaletteBuilder { - private final Context context; - private final Builder builder; - - PaletteBuilder(Builder builder, Context context) { - this.builder = builder; - this.context = context; - } - - public BitmapRequestBuilder build() { - - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) - .asBitmap() - .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(builder.song)); - } + @NonNull + public Builder checkIgnoreMediaStore() { + return ignoreMediaStore(PreferenceUtil.INSTANCE.isIgnoreMediaStoreArtwork()); } -} \ No newline at end of file + + @NonNull + public Builder ignoreMediaStore(boolean ignoreMediaStore) { + this.ignoreMediaStore = ignoreMediaStore; + return this; + } + + @NonNull + public DrawableRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(requestManager, song, ignoreMediaStore) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(song)); + } + } + + public static class BitmapBuilder { + private final Builder builder; + + BitmapBuilder(Builder builder) { + this.builder = builder; + } + + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) + .asBitmap() + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .dontTransform() + .signature(createSignature(builder.song)); + } + } + + public static class PaletteBuilder { + private final Context context; + private final Builder builder; + + PaletteBuilder(Builder builder, Context context) { + this.builder = builder; + this.context = context; + } + + public BitmapRequestBuilder build() { + + //noinspection unchecked + return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) + .asBitmap() + .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(builder.song)); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java index c11ffcc2d..b04854fac 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java @@ -17,20 +17,8 @@ package code.name.monkey.retromusic.glide; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; - import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; - -import com.bumptech.glide.BitmapRequestBuilder; -import com.bumptech.glide.DrawableRequestBuilder; -import com.bumptech.glide.DrawableTypeRequest; -import com.bumptech.glide.Priority; -import com.bumptech.glide.RequestManager; -import com.bumptech.glide.load.Key; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; -import com.bumptech.glide.request.target.Target; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.TintHelper; import code.name.monkey.retromusic.App; @@ -41,128 +29,143 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper; import code.name.monkey.retromusic.model.Artist; import code.name.monkey.retromusic.util.ArtistSignatureUtil; import code.name.monkey.retromusic.util.CustomArtistImageUtil; - +import com.bumptech.glide.BitmapRequestBuilder; +import com.bumptech.glide.DrawableRequestBuilder; +import com.bumptech.glide.DrawableTypeRequest; +import com.bumptech.glide.Priority; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.request.target.Target; public class ArtistGlideRequest { - private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; + private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; - private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.SOURCE; + private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.SOURCE; - private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_artist_art; + private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_artist_art; - @NonNull - private static Key createSignature(@NonNull Artist artist) { - return ArtistSignatureUtil.getInstance(App.Companion.getContext()).getArtistSignature(artist.getName()); + @NonNull + private static Key createSignature(@NonNull Artist artist) { + return ArtistSignatureUtil.getInstance(App.Companion.getContext()) + .getArtistSignature(artist.getName()); + } + + @NonNull + private static DrawableTypeRequest createBaseRequest( + @NonNull RequestManager requestManager, + @NonNull Artist artist, + boolean noCustomImage, + boolean forceDownload) { + boolean hasCustomImage = + CustomArtistImageUtil.Companion.getInstance(App.Companion.getContext()) + .hasCustomArtistImage(artist); + if (noCustomImage || !hasCustomImage) { + return requestManager.load(new ArtistImage(artist)); + } else { + return requestManager.load(CustomArtistImageUtil.getFile(artist)); + } + } + + public static class Builder { + final Artist artist; + final RequestManager requestManager; + private Drawable error; + private boolean forceDownload; + private boolean noCustomImage; + + private Builder(@NonNull RequestManager requestManager, Artist artist) { + this.requestManager = requestManager; + this.artist = artist; + error = + TintHelper.createTintedDrawable( + ContextCompat.getDrawable(App.Companion.getContext(), R.drawable.default_artist_art), + ThemeStore.Companion.accentColor(App.Companion.getContext())); } - @NonNull - private static DrawableTypeRequest createBaseRequest(@NonNull RequestManager requestManager, - @NonNull Artist artist, - boolean noCustomImage, boolean forceDownload) { - boolean hasCustomImage = CustomArtistImageUtil.Companion.getInstance(App.Companion.getContext()) - .hasCustomArtistImage(artist); - if (noCustomImage || !hasCustomImage) { - return requestManager.load(new ArtistImage(artist)); - } else { - return requestManager.load(CustomArtistImageUtil.getFile(artist)); - } + public static Builder from(@NonNull RequestManager requestManager, Artist artist) { + return new Builder(requestManager, artist); } - public static class Builder { - final Artist artist; - final RequestManager requestManager; - private Drawable error; - private boolean forceDownload; - private boolean noCustomImage; - - private Builder(@NonNull RequestManager requestManager, Artist artist) { - this.requestManager = requestManager; - this.artist = artist; - error = TintHelper.createTintedDrawable(ContextCompat.getDrawable(App.Companion.getContext(), R.drawable.default_artist_art), ThemeStore.Companion.accentColor(App.Companion.getContext())); - } - - public static Builder from(@NonNull RequestManager requestManager, Artist artist) { - return new Builder(requestManager, artist); - } - - public BitmapBuilder asBitmap() { - return new BitmapBuilder(this); - } - - public DrawableRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(requestManager, artist, noCustomImage, forceDownload) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .animate(DEFAULT_ANIMATION) - .error(DEFAULT_ERROR_IMAGE) - .priority(Priority.LOW) - .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .dontTransform() - .signature(createSignature(artist)); - } - - public Builder forceDownload(boolean forceDownload) { - this.forceDownload = forceDownload; - return this; - } - - public PaletteBuilder generatePalette(Context context) { - return new PaletteBuilder(this, context); - } - - public Builder noCustomImage(boolean noCustomImage) { - this.noCustomImage = noCustomImage; - return this; - } + public BitmapBuilder asBitmap() { + return new BitmapBuilder(this); } - public static class BitmapBuilder { - - private final Builder builder; - - BitmapBuilder(Builder builder) { - this.builder = builder; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.artist, builder.noCustomImage, - builder.forceDownload) - .asBitmap() - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .animate(DEFAULT_ANIMATION) - .error(DEFAULT_ERROR_IMAGE) - .priority(Priority.LOW) - .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .dontTransform() - .signature(createSignature(builder.artist)); - } + public DrawableRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(requestManager, artist, noCustomImage, forceDownload) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .animate(DEFAULT_ANIMATION) + .error(DEFAULT_ERROR_IMAGE) + .priority(Priority.LOW) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .dontTransform() + .signature(createSignature(artist)); } - public static class PaletteBuilder { - - final Context context; - - private final Builder builder; - - PaletteBuilder(Builder builder, Context context) { - this.builder = builder; - this.context = context; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.artist, builder.noCustomImage, - builder.forceDownload) - .asBitmap() - .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .priority(Priority.LOW) - .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .dontTransform() - .signature(createSignature(builder.artist)); - } + public Builder forceDownload(boolean forceDownload) { + this.forceDownload = forceDownload; + return this; } -} \ No newline at end of file + + public PaletteBuilder generatePalette(Context context) { + return new PaletteBuilder(this, context); + } + + public Builder noCustomImage(boolean noCustomImage) { + this.noCustomImage = noCustomImage; + return this; + } + } + + public static class BitmapBuilder { + + private final Builder builder; + + BitmapBuilder(Builder builder) { + this.builder = builder; + } + + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest( + builder.requestManager, builder.artist, builder.noCustomImage, builder.forceDownload) + .asBitmap() + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .animate(DEFAULT_ANIMATION) + .error(DEFAULT_ERROR_IMAGE) + .priority(Priority.LOW) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .dontTransform() + .signature(createSignature(builder.artist)); + } + } + + public static class PaletteBuilder { + + final Context context; + + private final Builder builder; + + PaletteBuilder(Builder builder, Context context) { + this.builder = builder; + this.context = context; + } + + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest( + builder.requestManager, builder.artist, builder.noCustomImage, builder.forceDownload) + .asBitmap() + .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .priority(Priority.LOW) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .dontTransform() + .signature(createSignature(builder.artist)); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/BlurTransformation.kt b/app/src/main/java/code/name/monkey/retromusic/glide/BlurTransformation.kt index 0c2af3dbb..0996b5532 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/BlurTransformation.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/BlurTransformation.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.glide import android.content.Context @@ -27,7 +27,6 @@ import code.name.monkey.retromusic.util.ImageUtil import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.bitmap.BitmapTransformation - class BlurTransformation : BitmapTransformation { private var context: Context? = null @@ -137,12 +136,10 @@ class BlurTransformation : BitmapTransformation { rs.destroy() return out - } catch (e: RSRuntimeException) { // on some devices RenderScript.create() throws: android.support.v8.renderscript.RSRuntimeException: Error loading libRSSupport library if (BuildConfig.DEBUG) e.printStackTrace() } - } return StackBlur.blur(out, blurRadius) @@ -155,4 +152,4 @@ class BlurTransformation : BitmapTransformation { companion object { val DEFAULT_BLUR_RADIUS = 5f } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/ProfileBannerGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/ProfileBannerGlideRequest.java index 27ec226d0..8a27576a3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/ProfileBannerGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/ProfileBannerGlideRequest.java @@ -1,79 +1,76 @@ package code.name.monkey.retromusic.glide; +import static code.name.monkey.retromusic.Constants.USER_BANNER; + import android.graphics.Bitmap; - import androidx.annotation.NonNull; - +import code.name.monkey.retromusic.App; +import code.name.monkey.retromusic.R; import com.bumptech.glide.BitmapRequestBuilder; import com.bumptech.glide.BitmapTypeRequest; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.signature.MediaStoreSignature; - import java.io.File; -import code.name.monkey.retromusic.App; -import code.name.monkey.retromusic.R; - -import static code.name.monkey.retromusic.Constants.USER_BANNER; - public class ProfileBannerGlideRequest { - private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; - private static final int DEFAULT_ERROR_IMAGE = R.drawable.material_design_default; - private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; + private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; + private static final int DEFAULT_ERROR_IMAGE = R.drawable.material_design_default; + private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; - public static File getBannerModel() { - File dir = App.Companion.getContext().getFilesDir(); - return new File(dir, USER_BANNER); + public static File getBannerModel() { + File dir = App.Companion.getContext().getFilesDir(); + return new File(dir, USER_BANNER); + } + + private static BitmapTypeRequest createBaseRequest( + RequestManager requestManager, File profile) { + return requestManager.load(profile).asBitmap(); + } + + private static Key createSignature(File file) { + return new MediaStoreSignature("", file.lastModified(), 0); + } + + public static class Builder { + private RequestManager requestManager; + private File profile; + + private Builder(RequestManager requestManager, File profile) { + this.requestManager = requestManager; + this.profile = profile; } - private static BitmapTypeRequest createBaseRequest(RequestManager requestManager, File profile) { - return requestManager.load(profile).asBitmap(); + public static Builder from(@NonNull RequestManager requestManager, File profile) { + return new Builder(requestManager, profile); } - private static Key createSignature(File file) { - return new MediaStoreSignature("", file.lastModified(), 0); + @NonNull + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(requestManager, profile) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .placeholder(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(profile)); + } + } + + public static class BitmapBuilder { + private final Builder builder; + + BitmapBuilder(Builder builder) { + this.builder = builder; } - public static class Builder { - private RequestManager requestManager; - private File profile; - - private Builder(RequestManager requestManager, File profile) { - this.requestManager = requestManager; - this.profile = profile; - } - - public static Builder from(@NonNull RequestManager requestManager, File profile) { - return new Builder(requestManager, profile); - } - - @NonNull - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(requestManager, profile) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .placeholder(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(profile)); - } - } - - public static class BitmapBuilder { - private final Builder builder; - - BitmapBuilder(Builder builder) { - this.builder = builder; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.profile) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(builder.profile)); - } + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(builder.requestManager, builder.profile) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(builder.profile)); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt index c6f740974..023dc396d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicColoredTarget.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.glide import android.graphics.drawable.Drawable @@ -24,7 +24,6 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.request.animation.GlideAnimation - abstract class RetroMusicColoredTarget(view: ImageView) : BitmapPaletteTarget(view) { protected val defaultFooterColor: Int diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicGlideModule.kt b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicGlideModule.kt index 078871cde..74fbe5655 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicGlideModule.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/RetroMusicGlideModule.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.glide import android.content.Context diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/SingleColorTarget.kt b/app/src/main/java/code/name/monkey/retromusic/glide/SingleColorTarget.kt index d4b2db1cc..dbe276248 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/SingleColorTarget.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/SingleColorTarget.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.glide import android.graphics.drawable.Drawable @@ -9,7 +23,6 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.util.ColorUtil import com.bumptech.glide.request.animation.GlideAnimation - abstract class SingleColorTarget(view: ImageView) : BitmapPaletteTarget(view) { protected val defaultFooterColor: Int @@ -36,4 +49,4 @@ abstract class SingleColorTarget(view: ImageView) : BitmapPaletteTarget(view) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/SongGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/SongGlideRequest.java index c1ff5df9a..5f424e4ae 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/SongGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/SongGlideRequest.java @@ -16,9 +16,14 @@ package code.name.monkey.retromusic.glide; import android.content.Context; import android.graphics.Bitmap; - import androidx.annotation.NonNull; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.glide.audiocover.AudioFileCover; +import code.name.monkey.retromusic.glide.palette.BitmapPaletteTranscoder; +import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper; +import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.util.MusicUtil; +import code.name.monkey.retromusic.util.PreferenceUtil; import com.bumptech.glide.BitmapRequestBuilder; import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.DrawableTypeRequest; @@ -28,122 +33,112 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.signature.MediaStoreSignature; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.glide.audiocover.AudioFileCover; -import code.name.monkey.retromusic.glide.palette.BitmapPaletteTranscoder; -import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper; -import code.name.monkey.retromusic.model.Song; -import code.name.monkey.retromusic.util.MusicUtil; -import code.name.monkey.retromusic.util.PreferenceUtil; - -/** - * Created by hemanths on 2019-09-15. - */ +/** Created by hemanths on 2019-09-15. */ public class SongGlideRequest { - private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; - private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_audio_art; - private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; + private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; + private static final int DEFAULT_ERROR_IMAGE = R.drawable.default_audio_art; + private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; - @NonNull - private static DrawableTypeRequest createBaseRequest(@NonNull RequestManager requestManager, - @NonNull Song song, - boolean ignoreMediaStore) { - if (ignoreMediaStore) { - return requestManager.load(new AudioFileCover(song.getData())); - } else { - return requestManager.loadFromMediaStore(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(song.getAlbumId())); - } + @NonNull + private static DrawableTypeRequest createBaseRequest( + @NonNull RequestManager requestManager, @NonNull Song song, boolean ignoreMediaStore) { + if (ignoreMediaStore) { + return requestManager.load(new AudioFileCover(song.getData())); + } else { + return requestManager.loadFromMediaStore( + MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(song.getAlbumId())); + } + } + + @NonNull + private static Key createSignature(@NonNull Song song) { + return new MediaStoreSignature("", song.getDateModified(), 0); + } + + public static class Builder { + final RequestManager requestManager; + final Song song; + boolean ignoreMediaStore; + + private Builder(@NonNull RequestManager requestManager, Song song) { + this.requestManager = requestManager; + this.song = song; } @NonNull - private static Key createSignature(@NonNull Song song) { - return new MediaStoreSignature("", song.getDateModified(), 0); + public static Builder from(@NonNull RequestManager requestManager, Song song) { + return new Builder(requestManager, song); } - public static class Builder { - final RequestManager requestManager; - final Song song; - boolean ignoreMediaStore; - - private Builder(@NonNull RequestManager requestManager, Song song) { - this.requestManager = requestManager; - this.song = song; - } - - @NonNull - public static Builder from(@NonNull RequestManager requestManager, Song song) { - return new Builder(requestManager, song); - } - - @NonNull - public PaletteBuilder generatePalette(@NonNull Context context) { - return new PaletteBuilder(this, context); - } - - @NonNull - public BitmapBuilder asBitmap() { - return new BitmapBuilder(this); - } - - @NonNull - public Builder checkIgnoreMediaStore(@NonNull Context context) { - return ignoreMediaStore(PreferenceUtil.INSTANCE.isIgnoreMediaStoreArtwork()); - } - - @NonNull - public Builder ignoreMediaStore(boolean ignoreMediaStore) { - this.ignoreMediaStore = ignoreMediaStore; - return this; - } - - @NonNull - public DrawableRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(requestManager, song, ignoreMediaStore) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(song)); - } + @NonNull + public PaletteBuilder generatePalette(@NonNull Context context) { + return new PaletteBuilder(this, context); } - public static class BitmapBuilder { - private final Builder builder; - - BitmapBuilder(Builder builder) { - this.builder = builder; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) - .asBitmap() - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(builder.song)); - } + @NonNull + public BitmapBuilder asBitmap() { + return new BitmapBuilder(this); } - public static class PaletteBuilder { - final Context context; - private final Builder builder; - - PaletteBuilder(Builder builder, Context context) { - this.builder = builder; - this.context = context; - } - - public BitmapRequestBuilder build() { - //noinspection unchecked - return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) - .asBitmap() - .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(DEFAULT_ERROR_IMAGE) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(builder.song)); - } + @NonNull + public Builder checkIgnoreMediaStore(@NonNull Context context) { + return ignoreMediaStore(PreferenceUtil.INSTANCE.isIgnoreMediaStoreArtwork()); } + + @NonNull + public Builder ignoreMediaStore(boolean ignoreMediaStore) { + this.ignoreMediaStore = ignoreMediaStore; + return this; + } + + @NonNull + public DrawableRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(requestManager, song, ignoreMediaStore) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(song)); + } + } + + public static class BitmapBuilder { + private final Builder builder; + + BitmapBuilder(Builder builder) { + this.builder = builder; + } + + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) + .asBitmap() + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(builder.song)); + } + } + + public static class PaletteBuilder { + final Context context; + private final Builder builder; + + PaletteBuilder(Builder builder, Context context) { + this.builder = builder; + this.context = context; + } + + public BitmapRequestBuilder build() { + //noinspection unchecked + return createBaseRequest(builder.requestManager, builder.song, builder.ignoreMediaStore) + .asBitmap() + .transcode(new BitmapPaletteTranscoder(context), BitmapPaletteWrapper.class) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(DEFAULT_ERROR_IMAGE) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(builder.song)); + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/UserProfileGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/UserProfileGlideRequest.java index 29e9b1a70..f3b2fd420 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/UserProfileGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/UserProfileGlideRequest.java @@ -1,82 +1,83 @@ package code.name.monkey.retromusic.glide; +import static code.name.monkey.retromusic.Constants.USER_PROFILE; + import android.graphics.Bitmap; import android.graphics.drawable.Drawable; - import androidx.annotation.NonNull; - +import code.name.monkey.appthemehelper.ThemeStore; +import code.name.monkey.appthemehelper.util.TintHelper; +import code.name.monkey.retromusic.App; +import code.name.monkey.retromusic.R; import com.bumptech.glide.BitmapRequestBuilder; import com.bumptech.glide.BitmapTypeRequest; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.signature.MediaStoreSignature; - import java.io.File; -import code.name.monkey.appthemehelper.ThemeStore; -import code.name.monkey.appthemehelper.util.TintHelper; -import code.name.monkey.retromusic.App; -import code.name.monkey.retromusic.R; - -import static code.name.monkey.retromusic.Constants.USER_PROFILE; - public class UserProfileGlideRequest { - private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; - private static final int DEFAULT_ERROR_IMAGE = R.drawable.ic_account; - private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; + private static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.NONE; + private static final int DEFAULT_ERROR_IMAGE = R.drawable.ic_account; + private static final int DEFAULT_ANIMATION = android.R.anim.fade_in; - public static File getUserModel() { - File dir = App.Companion.getContext().getFilesDir(); - return new File(dir, USER_PROFILE); + public static File getUserModel() { + File dir = App.Companion.getContext().getFilesDir(); + return new File(dir, USER_PROFILE); + } + + private static BitmapTypeRequest createBaseRequest( + RequestManager requestManager, File profile) { + return requestManager.load(profile).asBitmap(); + } + + private static Key createSignature(File file) { + return new MediaStoreSignature("", file.lastModified(), 0); + } + + public static class Builder { + private RequestManager requestManager; + private File profile; + private Drawable error; + + private Builder(RequestManager requestManager, File profile) { + this.requestManager = requestManager; + this.profile = profile; + error = + TintHelper.createTintedDrawable( + App.Companion.getContext(), + R.drawable.ic_account, + ThemeStore.Companion.accentColor(App.Companion.getContext())); } - private static BitmapTypeRequest createBaseRequest(RequestManager requestManager, File profile) { - return requestManager.load(profile).asBitmap(); + public static Builder from(@NonNull RequestManager requestManager, File profile) { + return new Builder(requestManager, profile); } - private static Key createSignature(File file) { - return new MediaStoreSignature("", file.lastModified(), 0); + @NonNull + public BitmapRequestBuilder build() { + return createBaseRequest(requestManager, profile) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(error) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(profile)); + } + } + + public static class BitmapBuilder { + private final Builder builder; + + BitmapBuilder(Builder builder) { + this.builder = builder; } - public static class Builder { - private RequestManager requestManager; - private File profile; - private Drawable error; - - private Builder(RequestManager requestManager, File profile) { - this.requestManager = requestManager; - this.profile = profile; - error = TintHelper.createTintedDrawable(App.Companion.getContext(), R.drawable.ic_account, ThemeStore.Companion.accentColor(App.Companion.getContext())); - } - - public static Builder from(@NonNull RequestManager requestManager, File profile) { - return new Builder(requestManager, profile); - } - - @NonNull - public BitmapRequestBuilder build() { - return createBaseRequest(requestManager, profile) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(error) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(profile)); - } - } - - public static class BitmapBuilder { - private final Builder builder; - - BitmapBuilder(Builder builder) { - this.builder = builder; - } - - public BitmapRequestBuilder build() { - return createBaseRequest(builder.requestManager, builder.profile) - .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) - .error(builder.error) - .animate(DEFAULT_ANIMATION) - .signature(createSignature(builder.profile)); - } + public BitmapRequestBuilder build() { + return createBaseRequest(builder.requestManager, builder.profile) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .error(builder.error) + .animate(DEFAULT_ANIMATION) + .signature(createSignature(builder.profile)); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index 9b67cb628..c07beeb70 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.glide.artistimage import android.content.Context @@ -28,12 +28,12 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.stream.StreamModelLoader -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor class ArtistImage(val artist: Artist) @@ -70,7 +70,7 @@ class ArtistImageFetcher( val response = deezerService.getArtistImage(artists[0]).execute() if (!response.isSuccessful) { - throw IOException("Request failed with code: " + response.code()) + throw IOException("Request failed with code: " + response.code()) } if (isCancelled) return null @@ -100,7 +100,6 @@ class ArtistImageFetcher( return context.contentResolver.openInputStream(imageUri) } - private fun getHighestQuality(imageUrl: Data): String { return when { imageUrl.pictureXl.isNotEmpty() -> imageUrl.pictureXl diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCover.java b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCover.java index cf3a82c5b..9a5fd6a05 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCover.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCover.java @@ -14,13 +14,11 @@ package code.name.monkey.retromusic.glide.audiocover; -/** - * @author Karim Abou Zeid (kabouzeid) - */ +/** @author Karim Abou Zeid (kabouzeid) */ public class AudioFileCover { - public final String filePath; + public final String filePath; - public AudioFileCover(String filePath) { - this.filePath = filePath; - } + public AudioFileCover(String filePath) { + this.filePath = filePath; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverFetcher.java b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverFetcher.java index 4ce9df5ca..5240d1d17 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverFetcher.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverFetcher.java @@ -15,64 +15,61 @@ package code.name.monkey.retromusic.glide.audiocover; import android.media.MediaMetadataRetriever; - import com.bumptech.glide.Priority; import com.bumptech.glide.load.data.DataFetcher; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; - public class AudioFileCoverFetcher implements DataFetcher { - private final AudioFileCover model; + private final AudioFileCover model; - private InputStream stream; + private InputStream stream; - public AudioFileCoverFetcher(AudioFileCover model) { + public AudioFileCoverFetcher(AudioFileCover model) { - this.model = model; + this.model = model; + } + + @Override + public String getId() { + // makes sure we never ever return null here + return String.valueOf(model.filePath); + } + + @Override + public InputStream loadData(final Priority priority) throws Exception { + + final MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setDataSource(model.filePath); + byte[] picture = retriever.getEmbeddedPicture(); + if (picture != null) { + stream = new ByteArrayInputStream(picture); + } else { + stream = AudioFileCoverUtils.fallback(model.filePath); + } + } finally { + retriever.release(); } - @Override - public String getId() { - // makes sure we never ever return null here - return String.valueOf(model.filePath); + return stream; + } + + @Override + public void cleanup() { + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + if (stream != null) { + try { + stream.close(); + } catch (IOException ignore) { + // can't do much about it + } } + } - @Override - public InputStream loadData(final Priority priority) throws Exception { - - final MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - try { - retriever.setDataSource(model.filePath); - byte[] picture = retriever.getEmbeddedPicture(); - if (picture != null) { - stream = new ByteArrayInputStream(picture); - } else { - stream = AudioFileCoverUtils.fallback(model.filePath); - } - } finally { - retriever.release(); - } - - return stream; - } - - @Override - public void cleanup() { - // already cleaned up in loadData and ByteArrayInputStream will be GC'd - if (stream != null) { - try { - stream.close(); - } catch (IOException ignore) { - // can't do much about it - } - } - } - - @Override - public void cancel() { - // cannot cancel - } + @Override + public void cancel() { + // cannot cancel + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverLoader.java b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverLoader.java index 1da3ec8c7..02ffb16d1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverLoader.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverLoader.java @@ -15,32 +15,28 @@ package code.name.monkey.retromusic.glide.audiocover; import android.content.Context; - import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GenericLoaderFactory; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.stream.StreamModelLoader; - import java.io.InputStream; - public class AudioFileCoverLoader implements StreamModelLoader { + @Override + public DataFetcher getResourceFetcher(AudioFileCover model, int width, int height) { + return new AudioFileCoverFetcher(model); + } + + public static class Factory implements ModelLoaderFactory { @Override - public DataFetcher getResourceFetcher(AudioFileCover model, int width, int height) { - return new AudioFileCoverFetcher(model); + public ModelLoader build( + Context context, GenericLoaderFactory factories) { + return new AudioFileCoverLoader(); } - public static class Factory implements ModelLoaderFactory { - @Override - public ModelLoader build(Context context, GenericLoaderFactory factories) { - return new AudioFileCoverLoader(); - } - - @Override - public void teardown() { - } - } + @Override + public void teardown() {} + } } - diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverUtils.java b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverUtils.java index 7fc6bbdd8..aaf612f1e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverUtils.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/audiocover/AudioFileCoverUtils.java @@ -14,50 +14,50 @@ package code.name.monkey.retromusic.glide.audiocover; -import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; -import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; -import org.jaudiotagger.audio.mp3.MP3File; -import org.jaudiotagger.tag.TagException; -import org.jaudiotagger.tag.images.Artwork; - import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; +import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; +import org.jaudiotagger.audio.mp3.MP3File; +import org.jaudiotagger.tag.TagException; +import org.jaudiotagger.tag.images.Artwork; public class AudioFileCoverUtils { - public static final String[] FALLBACKS = {"cover.jpg", "album.jpg", "folder.jpg", "cover.png", "album.png", "folder.png"}; + public static final String[] FALLBACKS = { + "cover.jpg", "album.jpg", "folder.jpg", "cover.png", "album.png", "folder.png" + }; - - public static InputStream fallback(String path) throws FileNotFoundException { - // Method 1: use embedded high resolution album art if there is any - try { - MP3File mp3File = new MP3File(path); - if (mp3File.hasID3v2Tag()) { - Artwork art = mp3File.getTag().getFirstArtwork(); - if (art != null) { - byte[] imageData = art.getBinaryData(); - return new ByteArrayInputStream(imageData); - } - } - // If there are any exceptions, we ignore them and continue to the other fallback method - } catch (ReadOnlyFileException ignored) { - } catch (InvalidAudioFrameException ignored) { - } catch (TagException ignored) { - } catch (IOException ignored) { + public static InputStream fallback(String path) throws FileNotFoundException { + // Method 1: use embedded high resolution album art if there is any + try { + MP3File mp3File = new MP3File(path); + if (mp3File.hasID3v2Tag()) { + Artwork art = mp3File.getTag().getFirstArtwork(); + if (art != null) { + byte[] imageData = art.getBinaryData(); + return new ByteArrayInputStream(imageData); } - - // Method 2: look for album art in external files - final File parent = new File(path).getParentFile(); - for (String fallback : FALLBACKS) { - File cover = new File(parent, fallback); - if (cover.exists()) { - return new FileInputStream(cover); - } - } - return null; + } + // If there are any exceptions, we ignore them and continue to the other fallback method + } catch (ReadOnlyFileException ignored) { + } catch (InvalidAudioFrameException ignored) { + } catch (TagException ignored) { + } catch (IOException ignored) { } -} \ No newline at end of file + + // Method 2: look for album art in external files + final File parent = new File(path).getParentFile(); + for (String fallback : FALLBACKS) { + File cover = new File(parent, fallback); + if (cover.exists()) { + return new FileInputStream(cover); + } + } + return null; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteResource.java b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteResource.java index dc954db20..d94c2c6da 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteResource.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteResource.java @@ -20,28 +20,28 @@ import com.bumptech.glide.util.Util; public class BitmapPaletteResource implements Resource { - private final BitmapPaletteWrapper bitmapPaletteWrapper; - private final BitmapPool bitmapPool; + private final BitmapPaletteWrapper bitmapPaletteWrapper; + private final BitmapPool bitmapPool; - public BitmapPaletteResource(BitmapPaletteWrapper bitmapPaletteWrapper, BitmapPool bitmapPool) { - this.bitmapPaletteWrapper = bitmapPaletteWrapper; - this.bitmapPool = bitmapPool; - } + public BitmapPaletteResource(BitmapPaletteWrapper bitmapPaletteWrapper, BitmapPool bitmapPool) { + this.bitmapPaletteWrapper = bitmapPaletteWrapper; + this.bitmapPool = bitmapPool; + } - @Override - public BitmapPaletteWrapper get() { - return bitmapPaletteWrapper; - } + @Override + public BitmapPaletteWrapper get() { + return bitmapPaletteWrapper; + } - @Override - public int getSize() { - return Util.getBitmapByteSize(bitmapPaletteWrapper.getBitmap()); - } + @Override + public int getSize() { + return Util.getBitmapByteSize(bitmapPaletteWrapper.getBitmap()); + } - @Override - public void recycle() { - if (!bitmapPool.put(bitmapPaletteWrapper.getBitmap())) { - bitmapPaletteWrapper.getBitmap().recycle(); - } + @Override + public void recycle() { + if (!bitmapPool.put(bitmapPaletteWrapper.getBitmap())) { + bitmapPaletteWrapper.getBitmap().recycle(); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTarget.java b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTarget.java index 5d478e67f..2ce727750 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTarget.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTarget.java @@ -15,16 +15,15 @@ package code.name.monkey.retromusic.glide.palette; import android.widget.ImageView; - import com.bumptech.glide.request.target.ImageViewTarget; public class BitmapPaletteTarget extends ImageViewTarget { - public BitmapPaletteTarget(ImageView view) { - super(view); - } + public BitmapPaletteTarget(ImageView view) { + super(view); + } - @Override - protected void setResource(BitmapPaletteWrapper bitmapPaletteWrapper) { - view.setImageBitmap(bitmapPaletteWrapper.getBitmap()); - } + @Override + protected void setResource(BitmapPaletteWrapper bitmapPaletteWrapper) { + view.setImageBitmap(bitmapPaletteWrapper.getBitmap()); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTranscoder.java b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTranscoder.java index 796bfa748..7fd4bfade 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTranscoder.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteTranscoder.java @@ -16,34 +16,33 @@ package code.name.monkey.retromusic.glide.palette; import android.content.Context; import android.graphics.Bitmap; - +import code.name.monkey.retromusic.util.RetroColorUtil; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; -import code.name.monkey.retromusic.util.RetroColorUtil; - public class BitmapPaletteTranscoder implements ResourceTranscoder { - private final BitmapPool bitmapPool; + private final BitmapPool bitmapPool; - public BitmapPaletteTranscoder(Context context) { - this(Glide.get(context).getBitmapPool()); - } + public BitmapPaletteTranscoder(Context context) { + this(Glide.get(context).getBitmapPool()); + } - public BitmapPaletteTranscoder(BitmapPool bitmapPool) { - this.bitmapPool = bitmapPool; - } + public BitmapPaletteTranscoder(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + } - @Override - public Resource transcode(Resource bitmapResource) { - Bitmap bitmap = bitmapResource.get(); - BitmapPaletteWrapper bitmapPaletteWrapper = new BitmapPaletteWrapper(bitmap, RetroColorUtil.generatePalette(bitmap)); - return new BitmapPaletteResource(bitmapPaletteWrapper, bitmapPool); - } + @Override + public Resource transcode(Resource bitmapResource) { + Bitmap bitmap = bitmapResource.get(); + BitmapPaletteWrapper bitmapPaletteWrapper = + new BitmapPaletteWrapper(bitmap, RetroColorUtil.generatePalette(bitmap)); + return new BitmapPaletteResource(bitmapPaletteWrapper, bitmapPool); + } - @Override - public String getId() { - return "BitmapPaletteTranscoder.com.kabouzeid.gramophone.glide.palette"; - } -} \ No newline at end of file + @Override + public String getId() { + return "BitmapPaletteTranscoder.com.kabouzeid.gramophone.glide.palette"; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteWrapper.java b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteWrapper.java index 105d09f06..df713937c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteWrapper.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/palette/BitmapPaletteWrapper.java @@ -15,23 +15,22 @@ package code.name.monkey.retromusic.glide.palette; import android.graphics.Bitmap; - import androidx.palette.graphics.Palette; public class BitmapPaletteWrapper { - private final Bitmap mBitmap; - private final Palette mPalette; + private final Bitmap mBitmap; + private final Palette mPalette; - public BitmapPaletteWrapper(Bitmap bitmap, Palette palette) { - mBitmap = bitmap; - mPalette = palette; - } + public BitmapPaletteWrapper(Bitmap bitmap, Palette palette) { + mBitmap = bitmap; + mPalette = palette; + } - public Bitmap getBitmap() { - return mBitmap; - } + public Bitmap getBitmap() { + return mBitmap; + } - public Palette getPalette() { - return mPalette; - } + public Palette getPalette() { + return mPalette; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt index fca8c2281..b155cbd99 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import android.content.Context import android.view.ViewGroup import code.name.monkey.retromusic.R - object HorizontalAdapterHelper { const val LAYOUT_RES = R.layout.item_album_card @@ -29,7 +28,8 @@ object HorizontalAdapterHelper { fun applyMarginToLayoutParams( context: Context, - layoutParams: ViewGroup.MarginLayoutParams, viewType: Int + layoutParams: ViewGroup.MarginLayoutParams, + viewType: Int ) { val listMargin = context.resources .getDimensionPixelSize(R.dimen.now_playing_top_margin) diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java b/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java index 9865cad24..f2956897b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java +++ b/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java @@ -15,8 +15,8 @@ package code.name.monkey.retromusic.helper; public interface M3UConstants { - String EXTENSION = "m3u"; - String HEADER = "#EXTM3U"; - String ENTRY = "#EXTINF:"; - String DURATION_SEPARATOR = ","; -} \ No newline at end of file + String EXTENSION = "m3u"; + String HEADER = "#EXTM3U"; + String ENTRY = "#EXTINF:"; + String DURATION_SEPARATOR = ","; +} diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.kt b/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.kt index 35e87d80d..249cd7786 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.kt @@ -1,15 +1,16 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ package code.name.monkey.retromusic.helper @@ -66,4 +67,4 @@ object M3UWriter : M3UConstants { } return file } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt index 30c6afa1b..fc815fe01 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import android.annotation.TargetApi @@ -29,10 +29,10 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.SongRepository import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.util.PreferenceUtil -import org.koin.core.KoinComponent -import org.koin.core.inject import java.io.File import java.util.* +import org.koin.core.KoinComponent +import org.koin.core.inject object MusicPlayerRemote : KoinComponent { val TAG: String = MusicPlayerRemote::class.java.simpleName @@ -41,7 +41,6 @@ object MusicPlayerRemote : KoinComponent { private val songRepository by inject() - @JvmStatic val isPlaying: Boolean get() = musicService != null && musicService!!.isPlaying @@ -442,7 +441,7 @@ object MusicPlayerRemote : KoinComponent { if (songs != null && songs.isNotEmpty()) { openQueue(songs, 0, true) } else { - //TODO the file is not listed in the media store + // TODO the file is not listed in the media store println("The file is not listed in the media store") } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt index bbc46cbe6..9cd2f3a9e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import android.os.Handler diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/PlayPauseButtonOnClickHandler.kt b/app/src/main/java/code/name/monkey/retromusic/helper/PlayPauseButtonOnClickHandler.kt index 24c1d351a..50be97474 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/PlayPauseButtonOnClickHandler.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/PlayPauseButtonOnClickHandler.kt @@ -1,22 +1,21 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import android.view.View - class PlayPauseButtonOnClickHandler : View.OnClickListener { override fun onClick(v: View) { if (MusicPlayerRemote.isPlaying) { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt index e4ad5a919..936af2b91 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import android.app.SearchManager @@ -19,9 +19,9 @@ import android.os.Bundle import android.provider.MediaStore import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.RealSongRepository +import java.util.* import org.koin.core.KoinComponent import org.koin.core.inject -import java.util.* object SearchQueryHelper : KoinComponent { private const val TITLE_SELECTION = "lower(" + MediaStore.Audio.AudioColumns.TITLE + ") = ?" diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/ShuffleHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/ShuffleHelper.kt index 6615f6194..02a621cba 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/ShuffleHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/ShuffleHelper.kt @@ -1,22 +1,21 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper import code.name.monkey.retromusic.model.Song - object ShuffleHelper { fun makeShuffleList(listToShuffle: MutableList, current: Int) { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt b/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt index 0d5eb6f1a..b02ded2ac 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt @@ -1,15 +1,16 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ package code.name.monkey.retromusic.helper @@ -55,8 +56,8 @@ class SortOrder { const val ALBUM_NUMBER_OF_SONGS = MediaStore.Audio.Albums.NUMBER_OF_SONGS + " DESC" /* Album sort order artist */ - const val ALBUM_ARTIST = (MediaStore.Audio.Artists.DEFAULT_SORT_ORDER - + ", " + MediaStore.Audio.Albums.DEFAULT_SORT_ORDER) + const val ALBUM_ARTIST = (MediaStore.Audio.Artists.DEFAULT_SORT_ORDER + + ", " + MediaStore.Audio.Albums.DEFAULT_SORT_ORDER) /* Album sort order year */ const val ALBUM_YEAR = MediaStore.Audio.Media.YEAR + " DESC" @@ -113,8 +114,8 @@ class SortOrder { const val SONG_Z_A = "$SONG_A_Z DESC" /* Album song sort order track list */ - const val SONG_TRACK_LIST = (MediaStore.Audio.Media.TRACK + ", " - + MediaStore.Audio.Media.DEFAULT_SORT_ORDER) + const val SONG_TRACK_LIST = (MediaStore.Audio.Media.TRACK + ", " + + MediaStore.Audio.Media.DEFAULT_SORT_ORDER) /* Album song sort order duration */ const val SONG_DURATION = SongSortOrder.SONG_DURATION @@ -183,4 +184,4 @@ class SortOrder { const val ALBUM_Z_A = "$GENRE_A_Z DESC" } } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/StackBlur.java b/app/src/main/java/code/name/monkey/retromusic/helper/StackBlur.java index 112b554bc..9756fa130 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/StackBlur.java +++ b/app/src/main/java/code/name/monkey/retromusic/helper/StackBlur.java @@ -1,7 +1,6 @@ package code.name.monkey.retromusic.helper; import android.graphics.Bitmap; - import java.util.ArrayList; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -9,325 +8,312 @@ import java.util.concurrent.Executors; /** * Blur using Java code. - *

- * This is a compromise between Gaussian Blur and Box blur - * It creates much better looking blurs than Box Blur, but is - * 7x faster than my Gaussian Blur implementation. - *

- * I called it Stack Blur because this describes best how this - * filter works internally: it creates a kind of moving stack - * of colors whilst scanning through the image. Thereby it - * just has to add one new block of color to the right side - * of the stack and remove the leftmost color. The remaining - * colors on the topmost layer of the stack are either added on - * or reduced by one, depending on if they are on the right or - * on the left side of the stack. * - * @author Enrique López Mañas - * http://www.neo-tech.es - *

- * Author of the original algorithm: Mario Klingemann - *

- * Based heavily on http://vitiy.info/Code/stackblur.cpp - * See http://vitiy.info/stackblur-algorithm-multi-threaded-blur-for-cpp/ + *

This is a compromise between Gaussian Blur and Box blur It creates much better looking blurs + * than Box Blur, but is 7x faster than my Gaussian Blur implementation. + * + *

I called it Stack Blur because this describes best how this filter works internally: it + * creates a kind of moving stack of colors whilst scanning through the image. Thereby it just has + * to add one new block of color to the right side of the stack and remove the leftmost color. The + * remaining colors on the topmost layer of the stack are either added on or reduced by one, + * depending on if they are on the right or on the left side of the stack. + * + * @author Enrique López Mañas http://www.neo-tech.es + *

Author of the original algorithm: Mario Klingemann + *

Based heavily on http://vitiy.info/Code/stackblur.cpp See + * http://vitiy.info/stackblur-algorithm-multi-threaded-blur-for-cpp/ * @copyright: Enrique López Mañas * @license: Apache License 2.0 */ public class StackBlur { - static final int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors(); - static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(EXECUTOR_THREADS); + static final int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors(); + static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(EXECUTOR_THREADS); - private static final short[] stackblur_mul = { - 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, - 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, - 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, - 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, - 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, - 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, - 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, - 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, - 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, - 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, - 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, - 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, - 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, - 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, - 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, - 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259 - }; + private static final short[] stackblur_mul = { + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, + 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, + 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, + 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, + 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, + 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, + 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, + 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, + 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, + 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, + 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, + 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, + 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, + 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, + 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259 + }; - private static final byte[] stackblur_shr = { - 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, - 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, - 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, - 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, - 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, - 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, - 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 - }; + private static final byte[] stackblur_shr = { + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 + }; - public static Bitmap blur(Bitmap original, float radius) { - int w = original.getWidth(); - int h = original.getHeight(); - int[] currentPixels = new int[w * h]; - original.getPixels(currentPixels, 0, w, 0, 0, w, h); - int cores = EXECUTOR_THREADS; + public static Bitmap blur(Bitmap original, float radius) { + int w = original.getWidth(); + int h = original.getHeight(); + int[] currentPixels = new int[w * h]; + original.getPixels(currentPixels, 0, w, 0, 0, w, h); + int cores = EXECUTOR_THREADS; - ArrayList horizontal = new ArrayList(cores); - ArrayList vertical = new ArrayList(cores); - for (int i = 0; i < cores; i++) { - horizontal.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 1)); - vertical.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 2)); - } - - try { - EXECUTOR.invokeAll(horizontal); - } catch (InterruptedException e) { - return null; - } - - try { - EXECUTOR.invokeAll(vertical); - } catch (InterruptedException e) { - return null; - } - - return Bitmap.createBitmap(currentPixels, w, h, Bitmap.Config.ARGB_8888); + ArrayList horizontal = new ArrayList(cores); + ArrayList vertical = new ArrayList(cores); + for (int i = 0; i < cores; i++) { + horizontal.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 1)); + vertical.add(new BlurTask(currentPixels, w, h, (int) radius, cores, i, 2)); } - private static void blurIteration(int[] src, int w, int h, int radius, int cores, int core, int step) { - int x, y, xp, yp, i; - int sp; - int stack_start; - int stack_i; - - int src_i; - int dst_i; - - long sum_r, sum_g, sum_b, - sum_in_r, sum_in_g, sum_in_b, - sum_out_r, sum_out_g, sum_out_b; - - int wm = w - 1; - int hm = h - 1; - int div = (radius * 2) + 1; - int mul_sum = stackblur_mul[radius]; - byte shr_sum = stackblur_shr[radius]; - int[] stack = new int[div]; - - if (step == 1) { - int minY = core * h / cores; - int maxY = (core + 1) * h / cores; - - for (y = minY; y < maxY; y++) { - sum_r = sum_g = sum_b = - sum_in_r = sum_in_g = sum_in_b = - sum_out_r = sum_out_g = sum_out_b = 0; - - src_i = w * y; // start of line (0,y) - - for (i = 0; i <= radius; i++) { - stack_i = i; - stack[stack_i] = src[src_i]; - sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); - sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); - sum_b += (src[src_i] & 0xff) * (i + 1); - sum_out_r += ((src[src_i] >>> 16) & 0xff); - sum_out_g += ((src[src_i] >>> 8) & 0xff); - sum_out_b += (src[src_i] & 0xff); - } - - - for (i = 1; i <= radius; i++) { - if (i <= wm) src_i += 1; - stack_i = i + radius; - stack[stack_i] = src[src_i]; - sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); - sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); - sum_b += (src[src_i] & 0xff) * (radius + 1 - i); - sum_in_r += ((src[src_i] >>> 16) & 0xff); - sum_in_g += ((src[src_i] >>> 8) & 0xff); - sum_in_b += (src[src_i] & 0xff); - } - - - sp = radius; - xp = radius; - if (xp > wm) xp = wm; - src_i = xp + y * w; // img.pix_ptr(xp, y); - dst_i = y * w; // img.pix_ptr(0, y); - for (x = 0; x < w; x++) { - src[dst_i] = (int) - ((src[dst_i] & 0xFFFFFFFF) | - ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) | - ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) | - ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); - dst_i += 1; - - sum_r -= sum_out_r; - sum_g -= sum_out_g; - sum_b -= sum_out_b; - - stack_start = sp + div - radius; - if (stack_start >= div) stack_start -= div; - stack_i = stack_start; - - sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); - sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); - sum_out_b -= (stack[stack_i] & 0xff); - - if (xp < wm) { - src_i += 1; - ++xp; - } - - stack[stack_i] = src[src_i]; - - sum_in_r += ((src[src_i] >>> 16) & 0xff); - sum_in_g += ((src[src_i] >>> 8) & 0xff); - sum_in_b += (src[src_i] & 0xff); - sum_r += sum_in_r; - sum_g += sum_in_g; - sum_b += sum_in_b; - - ++sp; - if (sp >= div) sp = 0; - stack_i = sp; - - sum_out_r += ((stack[stack_i] >>> 16) & 0xff); - sum_out_g += ((stack[stack_i] >>> 8) & 0xff); - sum_out_b += (stack[stack_i] & 0xff); - sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); - sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); - sum_in_b -= (stack[stack_i] & 0xff); - } - - } - } - - // step 2 - else if (step == 2) { - int minX = core * w / cores; - int maxX = (core + 1) * w / cores; - - for (x = minX; x < maxX; x++) { - sum_r = sum_g = sum_b = - sum_in_r = sum_in_g = sum_in_b = - sum_out_r = sum_out_g = sum_out_b = 0; - - src_i = x; // x,0 - for (i = 0; i <= radius; i++) { - stack_i = i; - stack[stack_i] = src[src_i]; - sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); - sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); - sum_b += (src[src_i] & 0xff) * (i + 1); - sum_out_r += ((src[src_i] >>> 16) & 0xff); - sum_out_g += ((src[src_i] >>> 8) & 0xff); - sum_out_b += (src[src_i] & 0xff); - } - for (i = 1; i <= radius; i++) { - if (i <= hm) src_i += w; // +stride - - stack_i = i + radius; - stack[stack_i] = src[src_i]; - sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); - sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); - sum_b += (src[src_i] & 0xff) * (radius + 1 - i); - sum_in_r += ((src[src_i] >>> 16) & 0xff); - sum_in_g += ((src[src_i] >>> 8) & 0xff); - sum_in_b += (src[src_i] & 0xff); - } - - sp = radius; - yp = radius; - if (yp > hm) yp = hm; - src_i = x + yp * w; // img.pix_ptr(x, yp); - dst_i = x; // img.pix_ptr(x, 0); - for (y = 0; y < h; y++) { - src[dst_i] = (int) - ((src[dst_i] & 0xFFFFFFFF) | - ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) | - ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) | - ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); - dst_i += w; - - sum_r -= sum_out_r; - sum_g -= sum_out_g; - sum_b -= sum_out_b; - - stack_start = sp + div - radius; - if (stack_start >= div) stack_start -= div; - stack_i = stack_start; - - sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); - sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); - sum_out_b -= (stack[stack_i] & 0xff); - - if (yp < hm) { - src_i += w; // stride - ++yp; - } - - stack[stack_i] = src[src_i]; - - sum_in_r += ((src[src_i] >>> 16) & 0xff); - sum_in_g += ((src[src_i] >>> 8) & 0xff); - sum_in_b += (src[src_i] & 0xff); - sum_r += sum_in_r; - sum_g += sum_in_g; - sum_b += sum_in_b; - - ++sp; - if (sp >= div) sp = 0; - stack_i = sp; - - sum_out_r += ((stack[stack_i] >>> 16) & 0xff); - sum_out_g += ((stack[stack_i] >>> 8) & 0xff); - sum_out_b += (stack[stack_i] & 0xff); - sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); - sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); - sum_in_b -= (stack[stack_i] & 0xff); - } - } - } - + try { + EXECUTOR.invokeAll(horizontal); + } catch (InterruptedException e) { + return null; } - private static class BlurTask implements Callable { - private final int[] _src; - private final int _w; - private final int _h; - private final int _radius; - private final int _totalCores; - private final int _coreIndex; - private final int _round; - - public BlurTask(int[] src, int w, int h, int radius, int totalCores, int coreIndex, int round) { - _src = src; - _w = w; - _h = h; - _radius = radius; - _totalCores = totalCores; - _coreIndex = coreIndex; - _round = round; - } - - @Override - public Void call() throws Exception { - blurIteration(_src, _w, _h, _radius, _totalCores, _coreIndex, _round); - return null; - } - + try { + EXECUTOR.invokeAll(vertical); + } catch (InterruptedException e) { + return null; } + + return Bitmap.createBitmap(currentPixels, w, h, Bitmap.Config.ARGB_8888); + } + + private static void blurIteration( + int[] src, int w, int h, int radius, int cores, int core, int step) { + int x, y, xp, yp, i; + int sp; + int stack_start; + int stack_i; + + int src_i; + int dst_i; + + long sum_r, sum_g, sum_b, sum_in_r, sum_in_g, sum_in_b, sum_out_r, sum_out_g, sum_out_b; + + int wm = w - 1; + int hm = h - 1; + int div = (radius * 2) + 1; + int mul_sum = stackblur_mul[radius]; + byte shr_sum = stackblur_shr[radius]; + int[] stack = new int[div]; + + if (step == 1) { + int minY = core * h / cores; + int maxY = (core + 1) * h / cores; + + for (y = minY; y < maxY; y++) { + sum_r = + sum_g = sum_b = sum_in_r = sum_in_g = sum_in_b = sum_out_r = sum_out_g = sum_out_b = 0; + + src_i = w * y; // start of line (0,y) + + for (i = 0; i <= radius; i++) { + stack_i = i; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); + sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); + sum_b += (src[src_i] & 0xff) * (i + 1); + sum_out_r += ((src[src_i] >>> 16) & 0xff); + sum_out_g += ((src[src_i] >>> 8) & 0xff); + sum_out_b += (src[src_i] & 0xff); + } + + for (i = 1; i <= radius; i++) { + if (i <= wm) src_i += 1; + stack_i = i + radius; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); + sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); + sum_b += (src[src_i] & 0xff) * (radius + 1 - i); + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + } + + sp = radius; + xp = radius; + if (xp > wm) xp = wm; + src_i = xp + y * w; // img.pix_ptr(xp, y); + dst_i = y * w; // img.pix_ptr(0, y); + for (x = 0; x < w; x++) { + src[dst_i] = + (int) + ((src[dst_i] & 0xFFFFFFFF) + | ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) + | ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) + | ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); + dst_i += 1; + + sum_r -= sum_out_r; + sum_g -= sum_out_g; + sum_b -= sum_out_b; + + stack_start = sp + div - radius; + if (stack_start >= div) stack_start -= div; + stack_i = stack_start; + + sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_out_b -= (stack[stack_i] & 0xff); + + if (xp < wm) { + src_i += 1; + ++xp; + } + + stack[stack_i] = src[src_i]; + + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + sum_r += sum_in_r; + sum_g += sum_in_g; + sum_b += sum_in_b; + + ++sp; + if (sp >= div) sp = 0; + stack_i = sp; + + sum_out_r += ((stack[stack_i] >>> 16) & 0xff); + sum_out_g += ((stack[stack_i] >>> 8) & 0xff); + sum_out_b += (stack[stack_i] & 0xff); + sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_in_b -= (stack[stack_i] & 0xff); + } + } + } + + // step 2 + else if (step == 2) { + int minX = core * w / cores; + int maxX = (core + 1) * w / cores; + + for (x = minX; x < maxX; x++) { + sum_r = + sum_g = sum_b = sum_in_r = sum_in_g = sum_in_b = sum_out_r = sum_out_g = sum_out_b = 0; + + src_i = x; // x,0 + for (i = 0; i <= radius; i++) { + stack_i = i; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); + sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); + sum_b += (src[src_i] & 0xff) * (i + 1); + sum_out_r += ((src[src_i] >>> 16) & 0xff); + sum_out_g += ((src[src_i] >>> 8) & 0xff); + sum_out_b += (src[src_i] & 0xff); + } + for (i = 1; i <= radius; i++) { + if (i <= hm) src_i += w; // +stride + + stack_i = i + radius; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); + sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); + sum_b += (src[src_i] & 0xff) * (radius + 1 - i); + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + } + + sp = radius; + yp = radius; + if (yp > hm) yp = hm; + src_i = x + yp * w; // img.pix_ptr(x, yp); + dst_i = x; // img.pix_ptr(x, 0); + for (y = 0; y < h; y++) { + src[dst_i] = + (int) + ((src[dst_i] & 0xFFFFFFFF) + | ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) + | ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) + | ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); + dst_i += w; + + sum_r -= sum_out_r; + sum_g -= sum_out_g; + sum_b -= sum_out_b; + + stack_start = sp + div - radius; + if (stack_start >= div) stack_start -= div; + stack_i = stack_start; + + sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_out_b -= (stack[stack_i] & 0xff); + + if (yp < hm) { + src_i += w; // stride + ++yp; + } + + stack[stack_i] = src[src_i]; + + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + sum_r += sum_in_r; + sum_g += sum_in_g; + sum_b += sum_in_b; + + ++sp; + if (sp >= div) sp = 0; + stack_i = sp; + + sum_out_r += ((stack[stack_i] >>> 16) & 0xff); + sum_out_g += ((stack[stack_i] >>> 8) & 0xff); + sum_out_b += (stack[stack_i] & 0xff); + sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_in_b -= (stack[stack_i] & 0xff); + } + } + } + } + + private static class BlurTask implements Callable { + private final int[] _src; + private final int _w; + private final int _h; + private final int _radius; + private final int _totalCores; + private final int _coreIndex; + private final int _round; + + public BlurTask(int[] src, int w, int h, int radius, int totalCores, int coreIndex, int round) { + _src = src; + _w = w; + _h = h; + _radius = radius; + _totalCores = totalCores; + _coreIndex = coreIndex; + _round = round; + } + + @Override + public Void call() throws Exception { + blurIteration(_src, _w, _h, _radius, _totalCores, _coreIndex, _round); + return null; + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/StopWatch.kt b/app/src/main/java/code/name/monkey/retromusic/helper/StopWatch.kt index 00062167d..b287bac49 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/StopWatch.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/StopWatch.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper /** diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt index 8f40adb5c..7aba2b09b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper.menu import android.view.MenuItem diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt index 971193e0c..92f35bf22 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt @@ -1,20 +1,19 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper.menu - import android.view.MenuItem import androidx.fragment.app.FragmentActivity import code.name.monkey.retromusic.R @@ -33,12 +32,12 @@ import kotlinx.coroutines.withContext import org.koin.core.KoinComponent import org.koin.core.get - object PlaylistMenuHelper : KoinComponent { fun handleMenuClick( activity: FragmentActivity, - playlistWithSongs: PlaylistWithSongs, item: MenuItem + playlistWithSongs: PlaylistWithSongs, + item: MenuItem ): Boolean { when (item.itemId) { R.id.action_play -> { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt index ac161880c..76a6b585b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongMenuHelper.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper.menu import android.content.Intent diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt index 912e3c81a..63ef58835 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/SongsMenuHelper.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.helper.menu import androidx.fragment.app.FragmentActivity diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt index 670a07de8..c5412d0f2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IAlbumClickListener.kt @@ -1,7 +1,21 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.interfaces import android.view.View interface IAlbumClickListener { fun onAlbumClick(albumId: Long, view: View) -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt index 31e98076e..f1c1f7450 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IArtistClickListener.kt @@ -1,7 +1,21 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.interfaces import android.view.View interface IArtistClickListener { fun onArtist(artistId: Long, view: View) -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt index c444933c3..810520358 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICabHolder.kt @@ -1,22 +1,21 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.interfaces import com.afollestad.materialcab.MaterialCab - interface ICabHolder { fun openCab(menuRes: Int, callback: MaterialCab.Callback): MaterialCab diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt index 205970f40..b44a01f0e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/ICallbacks.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic.interfaces import android.view.MenuItem @@ -10,4 +24,4 @@ interface ICallbacks { fun onFileMenuClicked(file: File, view: View) fun onMultipleItemAction(item: MenuItem, files: ArrayList) -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt index 034040143..14a93c47a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMainActivityFragmentCallbacks.kt @@ -1,15 +1,16 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ package code.name.monkey.retromusic.interfaces @@ -18,4 +19,4 @@ package code.name.monkey.retromusic.interfaces */ interface IMainActivityFragmentCallbacks { fun handleBackPress(): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt index 15e7a0399..29669a3a8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IMusicServiceEventListener.kt @@ -1,20 +1,19 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.interfaces - interface IMusicServiceEventListener { fun onServiceConnected() diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt index e05349738..82c617075 100644 --- a/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPaletteColorHolder.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.interfaces /** diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/Lrc.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/Lrc.java index 9405c68e2..b3a0c736c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/Lrc.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/Lrc.java @@ -1,30 +1,26 @@ package code.name.monkey.retromusic.lyrics; /** - * Desc : 歌词实体 - * Author : Lauzy - * Date : 2017/10/13 - * Blog : http://www.jianshu.com/u/e76853f863a9 - * Email : freedompaladin@gmail.com + * Desc : 歌词实体 Author : Lauzy Date : 2017/10/13 Blog : http://www.jianshu.com/u/e76853f863a9 Email : + * freedompaladin@gmail.com */ public class Lrc { - private long time; - private String text; + private long time; + private String text; - public long getTime() { - return time; - } + public long getTime() { + return time; + } - public void setTime(long time) { - this.time = time; - } + public void setTime(long time) { + this.time = time; + } - public String getText() { - return text; - } + public String getText() { + return text; + } - public void setText(String text) { - this.text = text; - } - -} \ No newline at end of file + public void setText(String text) { + this.text = text; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java index 66e200839..81682f8ad 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcEntry.java @@ -19,99 +19,94 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; -/** - * 一行歌词实体 - */ +/** 一行歌词实体 */ class LrcEntry implements Comparable { - public static final int GRAVITY_CENTER = 0; - public static final int GRAVITY_LEFT = 1; - public static final int GRAVITY_RIGHT = 2; - private long time; - private String text; - private String secondText; - private StaticLayout staticLayout; - /** - * 歌词距离视图顶部的距离 - */ - private float offset = Float.MIN_VALUE; + public static final int GRAVITY_CENTER = 0; + public static final int GRAVITY_LEFT = 1; + public static final int GRAVITY_RIGHT = 2; + private long time; + private String text; + private String secondText; + private StaticLayout staticLayout; + /** 歌词距离视图顶部的距离 */ + private float offset = Float.MIN_VALUE; - LrcEntry(long time, String text) { - this.time = time; - this.text = text; + LrcEntry(long time, String text) { + this.time = time; + this.text = text; + } + + LrcEntry(long time, String text, String secondText) { + this.time = time; + this.text = text; + this.secondText = secondText; + } + + void init(TextPaint paint, int width, int gravity) { + Layout.Alignment align; + switch (gravity) { + case GRAVITY_LEFT: + align = Layout.Alignment.ALIGN_NORMAL; + break; + + default: + case GRAVITY_CENTER: + align = Layout.Alignment.ALIGN_CENTER; + break; + + case GRAVITY_RIGHT: + align = Layout.Alignment.ALIGN_OPPOSITE; + break; } + staticLayout = new StaticLayout(getShowText(), paint, width, align, 1f, 0f, false); - LrcEntry(long time, String text, String secondText) { - this.time = time; - this.text = text; - this.secondText = secondText; + offset = Float.MIN_VALUE; + } + + long getTime() { + return time; + } + + StaticLayout getStaticLayout() { + return staticLayout; + } + + int getHeight() { + if (staticLayout == null) { + return 0; } + return staticLayout.getHeight(); + } - void init(TextPaint paint, int width, int gravity) { - Layout.Alignment align; - switch (gravity) { - case GRAVITY_LEFT: - align = Layout.Alignment.ALIGN_NORMAL; - break; + public float getOffset() { + return offset; + } - default: - case GRAVITY_CENTER: - align = Layout.Alignment.ALIGN_CENTER; - break; + public void setOffset(float offset) { + this.offset = offset; + } - case GRAVITY_RIGHT: - align = Layout.Alignment.ALIGN_OPPOSITE; - break; - } - staticLayout = new StaticLayout(getShowText(), paint, width, align, 1f, 0f, false); + String getText() { + return text; + } - offset = Float.MIN_VALUE; + void setSecondText(String secondText) { + this.secondText = secondText; + } + + private String getShowText() { + if (!TextUtils.isEmpty(secondText)) { + return text + "\n" + secondText; + } else { + return text; } + } - long getTime() { - return time; + @Override + public int compareTo(LrcEntry entry) { + if (entry == null) { + return -1; } - - StaticLayout getStaticLayout() { - return staticLayout; - } - - int getHeight() { - if (staticLayout == null) { - return 0; - } - return staticLayout.getHeight(); - } - - public float getOffset() { - return offset; - } - - public void setOffset(float offset) { - this.offset = offset; - } - - String getText() { - return text; - } - - - void setSecondText(String secondText) { - this.secondText = secondText; - } - - private String getShowText() { - if (!TextUtils.isEmpty(secondText)) { - return text + "\n" + secondText; - } else { - return text; - } - } - - @Override - public int compareTo(LrcEntry entry) { - if (entry == null) { - return -1; - } - return (int) (time - entry.getTime()); - } -} \ No newline at end of file + return (int) (time - entry.getTime()); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcHelper.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcHelper.java index cf0e99700..d1b71467d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcHelper.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcHelper.java @@ -1,7 +1,6 @@ package code.name.monkey.retromusic.lyrics; import android.content.Context; - import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -18,120 +17,121 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Desc : 歌词解析 - * Author : Lauzy - * Date : 2017/10/13 - * Blog : http://www.jianshu.com/u/e76853f863a9 - * Email : freedompaladin@gmail.com + * Desc : 歌词解析 Author : Lauzy Date : 2017/10/13 Blog : http://www.jianshu.com/u/e76853f863a9 Email : + * freedompaladin@gmail.com */ public class LrcHelper { - private static final String CHARSET = "utf-8"; - //[03:56.00][03:18.00][02:06.00][01:07.00]原谅我这一生不羁放纵爱自由 - private static final String LINE_REGEX = "((\\[\\d{2}:\\d{2}\\.\\d{2}])+)(.*)"; - private static final String TIME_REGEX = "\\[(\\d{2}):(\\d{2})\\.(\\d{2})]"; + private static final String CHARSET = "utf-8"; + // [03:56.00][03:18.00][02:06.00][01:07.00]原谅我这一生不羁放纵爱自由 + private static final String LINE_REGEX = "((\\[\\d{2}:\\d{2}\\.\\d{2}])+)(.*)"; + private static final String TIME_REGEX = "\\[(\\d{2}):(\\d{2})\\.(\\d{2})]"; - public static List parseLrcFromAssets(Context context, String fileName) { - try { - return parseInputStream(context.getResources().getAssets().open(fileName)); - } catch (IOException e) { - e.printStackTrace(); - } - return null; + public static List parseLrcFromAssets(Context context, String fileName) { + try { + return parseInputStream(context.getResources().getAssets().open(fileName)); + } catch (IOException e) { + e.printStackTrace(); } + return null; + } - public static List parseLrcFromFile(File file) { - try { - return parseInputStream(new FileInputStream(file)); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - return null; + public static List parseLrcFromFile(File file) { + try { + return parseInputStream(new FileInputStream(file)); + } catch (FileNotFoundException e) { + e.printStackTrace(); } + return null; + } - private static List parseInputStream(InputStream inputStream) { - List lrcs = new ArrayList<>(); - InputStreamReader isr = null; - BufferedReader br = null; - try { - isr = new InputStreamReader(inputStream, CHARSET); - br = new BufferedReader(isr); - String line; - while ((line = br.readLine()) != null) { - List lrcList = parseLrc(line); - if (lrcList != null && lrcList.size() != 0) { - lrcs.addAll(lrcList); - } - } - sortLrcs(lrcs); - return lrcs; - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (isr != null) { - isr.close(); - } - if (br != null) { - br.close(); - } - } catch (IOException e1) { - e1.printStackTrace(); - } + private static List parseInputStream(InputStream inputStream) { + List lrcs = new ArrayList<>(); + InputStreamReader isr = null; + BufferedReader br = null; + try { + isr = new InputStreamReader(inputStream, CHARSET); + br = new BufferedReader(isr); + String line; + while ((line = br.readLine()) != null) { + List lrcList = parseLrc(line); + if (lrcList != null && lrcList.size() != 0) { + lrcs.addAll(lrcList); } - return lrcs; + } + sortLrcs(lrcs); + return lrcs; + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (isr != null) { + isr.close(); + } + if (br != null) { + br.close(); + } + } catch (IOException e1) { + e1.printStackTrace(); + } } + return lrcs; + } - private static void sortLrcs(List lrcs) { - Collections.sort(lrcs, new Comparator() { - @Override - public int compare(Lrc o1, Lrc o2) { - return (int) (o1.getTime() - o2.getTime()); - } + private static void sortLrcs(List lrcs) { + Collections.sort( + lrcs, + new Comparator() { + @Override + public int compare(Lrc o1, Lrc o2) { + return (int) (o1.getTime() - o2.getTime()); + } }); + } + + private static List parseLrc(String lrcLine) { + if (lrcLine.trim().isEmpty()) { + return null; + } + List lrcs = new ArrayList<>(); + Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine); + if (!matcher.matches()) { + return null; } - private static List parseLrc(String lrcLine) { - if (lrcLine.trim().isEmpty()) { - return null; - } - List lrcs = new ArrayList<>(); - Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine); - if (!matcher.matches()) { - return null; - } + String time = matcher.group(1); + String content = matcher.group(3); + Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time); - String time = matcher.group(1); - String content = matcher.group(3); - Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time); - - while (timeMatcher.find()) { - String min = timeMatcher.group(1); - String sec = timeMatcher.group(2); - String mil = timeMatcher.group(3); - Lrc lrc = new Lrc(); - if (content != null && content.length() != 0) { - lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000 - + Long.parseLong(mil) * 10); - lrc.setText(content); - lrcs.add(lrc); - } - } - return lrcs; + while (timeMatcher.find()) { + String min = timeMatcher.group(1); + String sec = timeMatcher.group(2); + String mil = timeMatcher.group(3); + Lrc lrc = new Lrc(); + if (content != null && content.length() != 0) { + lrc.setTime( + Long.parseLong(min) * 60 * 1000 + + Long.parseLong(sec) * 1000 + + Long.parseLong(mil) * 10); + lrc.setText(content); + lrcs.add(lrc); + } } + return lrcs; + } - public static String formatTime(long time) { - int min = (int) (time / 60000); - int sec = (int) (time / 1000 % 60); - return adjustFormat(min) + ":" + adjustFormat(sec); - } + public static String formatTime(long time) { + int min = (int) (time / 60000); + int sec = (int) (time / 1000 % 60); + return adjustFormat(min) + ":" + adjustFormat(sec); + } - private static String adjustFormat(int time) { - if (time < 10) { - return "0" + time; - } - return time + ""; + private static String adjustFormat(int time) { + if (time < 10) { + return "0" + time; } -} \ No newline at end of file + return time + ""; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java index 23d7bfc3b..a54c6c2eb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcUtils.java @@ -17,7 +17,6 @@ package code.name.monkey.retromusic.lyrics; import android.animation.ValueAnimator; import android.text.TextUtils; import android.text.format.DateUtils; - import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -36,198 +35,186 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * 工具类 - */ +/** 工具类 */ class LrcUtils { - private static final Pattern PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)"); - private static final Pattern PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]"); + private static final Pattern PATTERN_LINE = + Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)"); + private static final Pattern PATTERN_TIME = + Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]"); - /** - * 从文件解析双语歌词 - */ - static List parseLrc(File[] lrcFiles) { - if (lrcFiles == null || lrcFiles.length != 2 || lrcFiles[0] == null) { - return null; - } - - File mainLrcFile = lrcFiles[0]; - File secondLrcFile = lrcFiles[1]; - List mainEntryList = parseLrc(mainLrcFile); - List secondEntryList = parseLrc(secondLrcFile); - - if (mainEntryList != null && secondEntryList != null) { - for (LrcEntry mainEntry : mainEntryList) { - for (LrcEntry secondEntry : secondEntryList) { - if (mainEntry.getTime() == secondEntry.getTime()) { - mainEntry.setSecondText(secondEntry.getText()); - } - } - } - } - return mainEntryList; + /** 从文件解析双语歌词 */ + static List parseLrc(File[] lrcFiles) { + if (lrcFiles == null || lrcFiles.length != 2 || lrcFiles[0] == null) { + return null; } - /** - * 从文件解析歌词 - */ - private static List parseLrc(File lrcFile) { - if (lrcFile == null || !lrcFile.exists()) { - return null; - } + File mainLrcFile = lrcFiles[0]; + File secondLrcFile = lrcFiles[1]; + List mainEntryList = parseLrc(mainLrcFile); + List secondEntryList = parseLrc(secondLrcFile); - List entryList = new ArrayList<>(); - try { - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(lrcFile), StandardCharsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - List list = parseLine(line); - if (list != null && !list.isEmpty()) { - entryList.addAll(list); - } - } - br.close(); - } catch (IOException e) { - e.printStackTrace(); + if (mainEntryList != null && secondEntryList != null) { + for (LrcEntry mainEntry : mainEntryList) { + for (LrcEntry secondEntry : secondEntryList) { + if (mainEntry.getTime() == secondEntry.getTime()) { + mainEntry.setSecondText(secondEntry.getText()); + } } + } + } + return mainEntryList; + } - Collections.sort(entryList); - return entryList; + /** 从文件解析歌词 */ + private static List parseLrc(File lrcFile) { + if (lrcFile == null || !lrcFile.exists()) { + return null; } - /** - * 从文本解析双语歌词 - */ - static List parseLrc(String[] lrcTexts) { - if (lrcTexts == null || lrcTexts.length != 2 || TextUtils.isEmpty(lrcTexts[0])) { - return null; + List entryList = new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new InputStreamReader(new FileInputStream(lrcFile), StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + List list = parseLine(line); + if (list != null && !list.isEmpty()) { + entryList.addAll(list); } - - String mainLrcText = lrcTexts[0]; - String secondLrcText = lrcTexts[1]; - List mainEntryList = parseLrc(mainLrcText); - List secondEntryList = parseLrc(secondLrcText); - - if (mainEntryList != null && secondEntryList != null) { - for (LrcEntry mainEntry : mainEntryList) { - for (LrcEntry secondEntry : secondEntryList) { - if (mainEntry.getTime() == secondEntry.getTime()) { - mainEntry.setSecondText(secondEntry.getText()); - } - } - } - } - return mainEntryList; + } + br.close(); + } catch (IOException e) { + e.printStackTrace(); } - /** - * 从文本解析歌词 - */ - private static List parseLrc(String lrcText) { - if (TextUtils.isEmpty(lrcText)) { - return null; - } + Collections.sort(entryList); + return entryList; + } - if (lrcText.startsWith("\uFEFF")) { - lrcText = lrcText.replace("\uFEFF", ""); - } - - List entryList = new ArrayList<>(); - String[] array = lrcText.split("\\n"); - for (String line : array) { - List list = parseLine(line); - if (list != null && !list.isEmpty()) { - entryList.addAll(list); - } - } - - Collections.sort(entryList); - return entryList; + /** 从文本解析双语歌词 */ + static List parseLrc(String[] lrcTexts) { + if (lrcTexts == null || lrcTexts.length != 2 || TextUtils.isEmpty(lrcTexts[0])) { + return null; } - /** - * 获取网络文本,需要在工作线程中执行 - */ - static String getContentFromNetwork(String url, String charset) { - String lrcText = null; - try { - URL _url = new URL(url); - HttpURLConnection conn = (HttpURLConnection) _url.openConnection(); - conn.setRequestMethod("GET"); - conn.setConnectTimeout(10000); - conn.setReadTimeout(10000); - if (conn.getResponseCode() == 200) { - InputStream is = conn.getInputStream(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int len; - while ((len = is.read(buffer)) != -1) { - bos.write(buffer, 0, len); - } - is.close(); - bos.close(); - lrcText = bos.toString(charset); - } - } catch (Exception e) { - e.printStackTrace(); + String mainLrcText = lrcTexts[0]; + String secondLrcText = lrcTexts[1]; + List mainEntryList = parseLrc(mainLrcText); + List secondEntryList = parseLrc(secondLrcText); + + if (mainEntryList != null && secondEntryList != null) { + for (LrcEntry mainEntry : mainEntryList) { + for (LrcEntry secondEntry : secondEntryList) { + if (mainEntry.getTime() == secondEntry.getTime()) { + mainEntry.setSecondText(secondEntry.getText()); + } } - return lrcText; + } + } + return mainEntryList; + } + + /** 从文本解析歌词 */ + private static List parseLrc(String lrcText) { + if (TextUtils.isEmpty(lrcText)) { + return null; } - /** - * 解析一行歌词 - */ - private static List parseLine(String line) { - if (TextUtils.isEmpty(line)) { - return null; - } - - line = line.trim(); - // [00:17.65]让我掉下眼泪的 - Matcher lineMatcher = PATTERN_LINE.matcher(line); - if (!lineMatcher.matches()) { - return null; - } - - String times = lineMatcher.group(1); - String text = lineMatcher.group(3); - List entryList = new ArrayList<>(); - - // [00:17.65] - Matcher timeMatcher = PATTERN_TIME.matcher(times); - while (timeMatcher.find()) { - long min = Long.parseLong(timeMatcher.group(1)); - long sec = Long.parseLong(timeMatcher.group(2)); - String milString = timeMatcher.group(3); - long mil = Long.parseLong(milString); - // 如果毫秒是两位数,需要乘以10 - if (milString.length() == 2) { - mil = mil * 10; - } - long time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil; - entryList.add(new LrcEntry(time, text)); - } - return entryList; + if (lrcText.startsWith("\uFEFF")) { + lrcText = lrcText.replace("\uFEFF", ""); } - /** - * 转为[分:秒] - */ - static String formatTime(long milli) { - int m = (int) (milli / DateUtils.MINUTE_IN_MILLIS); - int s = (int) ((milli / DateUtils.SECOND_IN_MILLIS) % 60); - String mm = String.format(Locale.getDefault(), "%02d", m); - String ss = String.format(Locale.getDefault(), "%02d", s); - return mm + ":" + ss; + List entryList = new ArrayList<>(); + String[] array = lrcText.split("\\n"); + for (String line : array) { + List list = parseLine(line); + if (list != null && !list.isEmpty()) { + entryList.addAll(list); + } } - static void resetDurationScale() { - try { - Field mField = ValueAnimator.class.getDeclaredField("sDurationScale"); - mField.setAccessible(true); - mField.setFloat(null, 1); - } catch (Exception e) { - e.printStackTrace(); + Collections.sort(entryList); + return entryList; + } + + /** 获取网络文本,需要在工作线程中执行 */ + static String getContentFromNetwork(String url, String charset) { + String lrcText = null; + try { + URL _url = new URL(url); + HttpURLConnection conn = (HttpURLConnection) _url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + if (conn.getResponseCode() == 200) { + InputStream is = conn.getInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = is.read(buffer)) != -1) { + bos.write(buffer, 0, len); } + is.close(); + bos.close(); + lrcText = bos.toString(charset); + } + } catch (Exception e) { + e.printStackTrace(); } -} \ No newline at end of file + return lrcText; + } + + /** 解析一行歌词 */ + private static List parseLine(String line) { + if (TextUtils.isEmpty(line)) { + return null; + } + + line = line.trim(); + // [00:17.65]让我掉下眼泪的 + Matcher lineMatcher = PATTERN_LINE.matcher(line); + if (!lineMatcher.matches()) { + return null; + } + + String times = lineMatcher.group(1); + String text = lineMatcher.group(3); + List entryList = new ArrayList<>(); + + // [00:17.65] + Matcher timeMatcher = PATTERN_TIME.matcher(times); + while (timeMatcher.find()) { + long min = Long.parseLong(timeMatcher.group(1)); + long sec = Long.parseLong(timeMatcher.group(2)); + String milString = timeMatcher.group(3); + long mil = Long.parseLong(milString); + // 如果毫秒是两位数,需要乘以10 + if (milString.length() == 2) { + mil = mil * 10; + } + long time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil; + entryList.add(new LrcEntry(time, text)); + } + return entryList; + } + + /** 转为[分:秒] */ + static String formatTime(long milli) { + int m = (int) (milli / DateUtils.MINUTE_IN_MILLIS); + int s = (int) ((milli / DateUtils.SECOND_IN_MILLIS) % 60); + String mm = String.format(Locale.getDefault(), "%02d", m); + String ss = String.format(Locale.getDefault(), "%02d", s); + return mm + ":" + ss; + } + + static void resetDurationScale() { + try { + Field mField = ValueAnimator.class.getDeclaredField("sDurationScale"); + mField.setAccessible(true); + mField.setFloat(null, 1); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java index 63c4b8cfc..92eec91f9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/LrcView.java @@ -34,720 +34,739 @@ import android.view.MotionEvent; import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.Scroller; - +import code.name.monkey.retromusic.BuildConfig; +import code.name.monkey.retromusic.R; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import code.name.monkey.retromusic.BuildConfig; -import code.name.monkey.retromusic.R; - -/** - * 歌词 - * Created by wcy on 2015/11/9. - */ +/** 歌词 Created by wcy on 2015/11/9. */ @SuppressLint("StaticFieldLeak") public class LrcView extends View { - private static final long ADJUST_DURATION = 100; - private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS; + private static final long ADJUST_DURATION = 100; + private static final long TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS; - private List mLrcEntryList = new ArrayList<>(); - private TextPaint mLrcPaint = new TextPaint(); - private TextPaint mTimePaint = new TextPaint(); - private Paint.FontMetrics mTimeFontMetrics; - private Drawable mPlayDrawable; - private float mDividerHeight; - private long mAnimationDuration; - private int mNormalTextColor; - private float mNormalTextSize; - private int mCurrentTextColor; - private float mCurrentTextSize; - private int mTimelineTextColor; - private int mTimelineColor; - private int mTimeTextColor; - private int mDrawableWidth; - private int mTimeTextWidth; - private String mDefaultLabel; - private float mLrcPadding; - private OnPlayClickListener mOnPlayClickListener; - private ValueAnimator mAnimator; - private GestureDetector mGestureDetector; - private Scroller mScroller; - private float mOffset; - private int mCurrentLine; - private Object mFlag; - private boolean isShowTimeline; - private boolean isTouching; - private boolean isFling; - private int mTextGravity;//歌词显示位置,靠左/居中/靠右 - private Runnable hideTimelineRunnable = new Runnable() { + private List mLrcEntryList = new ArrayList<>(); + private TextPaint mLrcPaint = new TextPaint(); + private TextPaint mTimePaint = new TextPaint(); + private Paint.FontMetrics mTimeFontMetrics; + private Drawable mPlayDrawable; + private float mDividerHeight; + private long mAnimationDuration; + private int mNormalTextColor; + private float mNormalTextSize; + private int mCurrentTextColor; + private float mCurrentTextSize; + private int mTimelineTextColor; + private int mTimelineColor; + private int mTimeTextColor; + private int mDrawableWidth; + private int mTimeTextWidth; + private String mDefaultLabel; + private float mLrcPadding; + private OnPlayClickListener mOnPlayClickListener; + private ValueAnimator mAnimator; + private GestureDetector mGestureDetector; + private Scroller mScroller; + private float mOffset; + private int mCurrentLine; + private Object mFlag; + private boolean isShowTimeline; + private boolean isTouching; + private boolean isFling; + private int mTextGravity; // 歌词显示位置,靠左/居中/靠右 + private Runnable hideTimelineRunnable = + new Runnable() { @Override public void run() { - if (hasLrc() && isShowTimeline) { - isShowTimeline = false; - smoothScrollTo(mCurrentLine); - } + if (hasLrc() && isShowTimeline) { + isShowTimeline = false; + smoothScrollTo(mCurrentLine); + } } - }; - /** - * 手势监听器 - */ - private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { + }; + /** 手势监听器 */ + private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = + new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { - if (hasLrc() && mOnPlayClickListener != null) { - mScroller.forceFinished(true); - removeCallbacks(hideTimelineRunnable); - isTouching = true; - isShowTimeline = true; - invalidate(); - return true; - } - return super.onDown(e); + if (hasLrc() && mOnPlayClickListener != null) { + mScroller.forceFinished(true); + removeCallbacks(hideTimelineRunnable); + isTouching = true; + isShowTimeline = true; + invalidate(); + return true; + } + return super.onDown(e); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (hasLrc()) { - mOffset += -distanceY; - mOffset = Math.min(mOffset, getOffset(0)); - mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1)); - invalidate(); - return true; - } - return super.onScroll(e1, e2, distanceX, distanceY); + if (hasLrc()) { + mOffset += -distanceY; + mOffset = Math.min(mOffset, getOffset(0)); + mOffset = Math.max(mOffset, getOffset(mLrcEntryList.size() - 1)); + invalidate(); + return true; + } + return super.onScroll(e1, e2, distanceX, distanceY); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (hasLrc()) { - mScroller.fling(0, (int) mOffset, 0, (int) velocityY, 0, 0, (int) getOffset(mLrcEntryList.size() - 1), (int) getOffset(0)); - isFling = true; - return true; - } - return super.onFling(e1, e2, velocityX, velocityY); + if (hasLrc()) { + mScroller.fling( + 0, + (int) mOffset, + 0, + (int) velocityY, + 0, + 0, + (int) getOffset(mLrcEntryList.size() - 1), + (int) getOffset(0)); + isFling = true; + return true; + } + return super.onFling(e1, e2, velocityX, velocityY); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { - if (hasLrc() && isShowTimeline && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) { - int centerLine = getCenterLine(); - long centerLineTime = mLrcEntryList.get(centerLine).getTime(); - // onPlayClick 消费了才更新 UI - if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) { - isShowTimeline = false; - removeCallbacks(hideTimelineRunnable); - mCurrentLine = centerLine; - invalidate(); - return true; - } + if (hasLrc() + && isShowTimeline + && mPlayDrawable.getBounds().contains((int) e.getX(), (int) e.getY())) { + int centerLine = getCenterLine(); + long centerLineTime = mLrcEntryList.get(centerLine).getTime(); + // onPlayClick 消费了才更新 UI + if (mOnPlayClickListener != null && mOnPlayClickListener.onPlayClick(centerLineTime)) { + isShowTimeline = false; + removeCallbacks(hideTimelineRunnable); + mCurrentLine = centerLine; + invalidate(); + return true; } - return super.onSingleTapConfirmed(e); + } + return super.onSingleTapConfirmed(e); } - }; + }; - public LrcView(Context context) { - this(context, null); + public LrcView(Context context) { + this(context, null); + } + + public LrcView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LrcView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + private void init(AttributeSet attrs) { + TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LrcView); + mCurrentTextSize = + ta.getDimension( + R.styleable.LrcView_lrcTextSize, getResources().getDimension(R.dimen.lrc_text_size)); + mNormalTextSize = + ta.getDimension( + R.styleable.LrcView_lrcNormalTextSize, + getResources().getDimension(R.dimen.lrc_text_size)); + if (mNormalTextSize == 0) { + mNormalTextSize = mCurrentTextSize; } - public LrcView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } + mDividerHeight = + ta.getDimension( + R.styleable.LrcView_lrcDividerHeight, + getResources().getDimension(R.dimen.lrc_divider_height)); + int defDuration = getResources().getInteger(R.integer.lrc_animation_duration); + mAnimationDuration = ta.getInt(R.styleable.LrcView_lrcAnimationDuration, defDuration); + mAnimationDuration = (mAnimationDuration < 0) ? defDuration : mAnimationDuration; + mNormalTextColor = + ta.getColor( + R.styleable.LrcView_lrcNormalTextColor, + getResources().getColor(R.color.lrc_normal_text_color)); + mCurrentTextColor = + ta.getColor( + R.styleable.LrcView_lrcCurrentTextColor, + getResources().getColor(R.color.lrc_current_text_color)); + mTimelineTextColor = + ta.getColor( + R.styleable.LrcView_lrcTimelineTextColor, + getResources().getColor(R.color.lrc_timeline_text_color)); + mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel); + mDefaultLabel = + TextUtils.isEmpty(mDefaultLabel) ? getContext().getString(R.string.empty) : mDefaultLabel; + mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0); + mTimelineColor = + ta.getColor( + R.styleable.LrcView_lrcTimelineColor, + getResources().getColor(R.color.lrc_timeline_color)); + float timelineHeight = + ta.getDimension( + R.styleable.LrcView_lrcTimelineHeight, + getResources().getDimension(R.dimen.lrc_timeline_height)); + mPlayDrawable = ta.getDrawable(R.styleable.LrcView_lrcPlayDrawable); + mPlayDrawable = + (mPlayDrawable == null) + ? getResources().getDrawable(R.drawable.ic_play_arrow) + : mPlayDrawable; + mTimeTextColor = + ta.getColor( + R.styleable.LrcView_lrcTimeTextColor, + getResources().getColor(R.color.lrc_time_text_color)); + float timeTextSize = + ta.getDimension( + R.styleable.LrcView_lrcTimeTextSize, + getResources().getDimension(R.dimen.lrc_time_text_size)); + mTextGravity = ta.getInteger(R.styleable.LrcView_lrcTextGravity, LrcEntry.GRAVITY_CENTER); - public LrcView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(attrs); - } + ta.recycle(); - private void init(AttributeSet attrs) { - TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LrcView); - mCurrentTextSize = ta.getDimension(R.styleable.LrcView_lrcTextSize, getResources().getDimension(R.dimen.lrc_text_size)); - mNormalTextSize = ta.getDimension(R.styleable.LrcView_lrcNormalTextSize, getResources().getDimension(R.dimen.lrc_text_size)); - if (mNormalTextSize == 0) { - mNormalTextSize = mCurrentTextSize; + mDrawableWidth = (int) getResources().getDimension(R.dimen.lrc_drawable_width); + mTimeTextWidth = (int) getResources().getDimension(R.dimen.lrc_time_width); + + mLrcPaint.setAntiAlias(true); + mLrcPaint.setTextSize(mCurrentTextSize); + mLrcPaint.setTextAlign(Paint.Align.LEFT); + mTimePaint.setAntiAlias(true); + mTimePaint.setTextSize(timeTextSize); + mTimePaint.setTextAlign(Paint.Align.CENTER); + //noinspection SuspiciousNameCombination + mTimePaint.setStrokeWidth(timelineHeight); + mTimePaint.setStrokeCap(Paint.Cap.ROUND); + mTimeFontMetrics = mTimePaint.getFontMetrics(); + + mGestureDetector = new GestureDetector(getContext(), mSimpleOnGestureListener); + mGestureDetector.setIsLongpressEnabled(false); + mScroller = new Scroller(getContext()); + } + + /** 设置非当前行歌词字体颜色 */ + public void setNormalColor(int normalColor) { + mNormalTextColor = normalColor; + postInvalidate(); + } + + /** 普通歌词文本字体大小 */ + public void setNormalTextSize(float size) { + mNormalTextSize = size; + } + + /** 当前歌词文本字体大小 */ + public void setCurrentTextSize(float size) { + mCurrentTextSize = size; + } + + /** 设置当前行歌词的字体颜色 */ + public void setCurrentColor(int currentColor) { + mCurrentTextColor = currentColor; + postInvalidate(); + } + + /** 设置拖动歌词时选中歌词的字体颜色 */ + public void setTimelineTextColor(int timelineTextColor) { + mTimelineTextColor = timelineTextColor; + postInvalidate(); + } + + /** 设置拖动歌词时时间线的颜色 */ + public void setTimelineColor(int timelineColor) { + mTimelineColor = timelineColor; + postInvalidate(); + } + + /** 设置拖动歌词时右侧时间字体颜色 */ + public void setTimeTextColor(int timeTextColor) { + mTimeTextColor = timeTextColor; + postInvalidate(); + } + + /** + * 设置歌词是否允许拖动 + * + * @param draggable 是否允许拖动 + * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null + */ + public void setDraggable(boolean draggable, OnPlayClickListener onPlayClickListener) { + if (draggable) { + if (onPlayClickListener == null) { + throw new IllegalArgumentException( + "if draggable == true, onPlayClickListener must not be null"); + } + mOnPlayClickListener = onPlayClickListener; + } else { + mOnPlayClickListener = null; + } + } + + /** + * 设置播放按钮点击监听器 + * + * @param onPlayClickListener 如果为非 null ,则激活歌词拖动功能,否则将将禁用歌词拖动功能 + * @deprecated use {@link #setDraggable(boolean, OnPlayClickListener)} instead + */ + @Deprecated + public void setOnPlayClickListener(OnPlayClickListener onPlayClickListener) { + mOnPlayClickListener = onPlayClickListener; + } + + /** 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” */ + public void setLabel(String label) { + runOnUi( + () -> { + mDefaultLabel = label; + invalidate(); + }); + } + + /** + * 加载歌词文件 + * + * @param lrcFile 歌词文件 + */ + public void loadLrc(File lrcFile) { + loadLrc(lrcFile, null); + } + + /** + * 加载双语歌词文件,两种语言的歌词时间戳需要一致 + * + * @param mainLrcFile 第一种语言歌词文件 + * @param secondLrcFile 第二种语言歌词文件 + */ + public void loadLrc(File mainLrcFile, File secondLrcFile) { + runOnUi( + () -> { + reset(); + + StringBuilder sb = new StringBuilder("file://"); + sb.append(mainLrcFile.getPath()); + if (secondLrcFile != null) { + sb.append("#").append(secondLrcFile.getPath()); + } + String flag = sb.toString(); + setFlag(flag); + new AsyncTask>() { + @Override + protected List doInBackground(File... params) { + return LrcUtils.parseLrc(params); + } + + @Override + protected void onPostExecute(List lrcEntries) { + if (getFlag() == flag) { + onLrcLoaded(lrcEntries); + setFlag(null); + } + } + }.execute(mainLrcFile, secondLrcFile); + }); + } + + /** + * 加载歌词文本 + * + * @param lrcText 歌词文本 + */ + public void loadLrc(String lrcText) { + loadLrc(lrcText, null); + } + + /** + * 加载双语歌词文本,两种语言的歌词时间戳需要一致 + * + * @param mainLrcText 第一种语言歌词文本 + * @param secondLrcText 第二种语言歌词文本 + */ + public void loadLrc(String mainLrcText, String secondLrcText) { + runOnUi( + () -> { + reset(); + + StringBuilder sb = new StringBuilder("file://"); + sb.append(mainLrcText); + if (secondLrcText != null) { + sb.append("#").append(secondLrcText); + } + String flag = sb.toString(); + setFlag(flag); + new AsyncTask>() { + @Override + protected List doInBackground(String... params) { + return LrcUtils.parseLrc(params); + } + + @Override + protected void onPostExecute(List lrcEntries) { + if (getFlag() == flag) { + onLrcLoaded(lrcEntries); + setFlag(null); + } + } + }.execute(mainLrcText, secondLrcText); + }); + } + + /** + * 加载在线歌词,默认使用 utf-8 编码 + * + * @param lrcUrl 歌词文件的网络地址 + */ + public void loadLrcByUrl(String lrcUrl) { + loadLrcByUrl(lrcUrl, "utf-8"); + } + + /** + * 加载在线歌词 + * + * @param lrcUrl 歌词文件的网络地址 + * @param charset 编码格式 + */ + public void loadLrcByUrl(String lrcUrl, String charset) { + String flag = "url://" + lrcUrl; + setFlag(flag); + new AsyncTask() { + @Override + protected String doInBackground(String... params) { + return LrcUtils.getContentFromNetwork(params[0], params[1]); + } + + @Override + protected void onPostExecute(String lrcText) { + if (getFlag() == flag) { + loadLrc(lrcText); } + } + }.execute(lrcUrl, charset); + } - mDividerHeight = ta.getDimension(R.styleable.LrcView_lrcDividerHeight, getResources().getDimension(R.dimen.lrc_divider_height)); - int defDuration = getResources().getInteger(R.integer.lrc_animation_duration); - mAnimationDuration = ta.getInt(R.styleable.LrcView_lrcAnimationDuration, defDuration); - mAnimationDuration = (mAnimationDuration < 0) ? defDuration : mAnimationDuration; - mNormalTextColor = ta.getColor(R.styleable.LrcView_lrcNormalTextColor, getResources().getColor(R.color.lrc_normal_text_color)); - mCurrentTextColor = ta.getColor(R.styleable.LrcView_lrcCurrentTextColor, getResources().getColor(R.color.lrc_current_text_color)); - mTimelineTextColor = ta.getColor(R.styleable.LrcView_lrcTimelineTextColor, getResources().getColor(R.color.lrc_timeline_text_color)); - mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel); - mDefaultLabel = TextUtils.isEmpty(mDefaultLabel) ? getContext().getString(R.string.empty) : mDefaultLabel; - mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0); - mTimelineColor = ta.getColor(R.styleable.LrcView_lrcTimelineColor, getResources().getColor(R.color.lrc_timeline_color)); - float timelineHeight = ta.getDimension(R.styleable.LrcView_lrcTimelineHeight, getResources().getDimension(R.dimen.lrc_timeline_height)); - mPlayDrawable = ta.getDrawable(R.styleable.LrcView_lrcPlayDrawable); - mPlayDrawable = (mPlayDrawable == null) ? getResources().getDrawable(R.drawable.ic_play_arrow) : mPlayDrawable; - mTimeTextColor = ta.getColor(R.styleable.LrcView_lrcTimeTextColor, getResources().getColor(R.color.lrc_time_text_color)); - float timeTextSize = ta.getDimension(R.styleable.LrcView_lrcTimeTextSize, getResources().getDimension(R.dimen.lrc_time_text_size)); - mTextGravity = ta.getInteger(R.styleable.LrcView_lrcTextGravity, LrcEntry.GRAVITY_CENTER); + /** + * 歌词是否有效 + * + * @return true,如果歌词有效,否则false + */ + public boolean hasLrc() { + return !mLrcEntryList.isEmpty(); + } - ta.recycle(); + /** + * 刷新歌词 + * + * @param time 当前播放时间 + */ + public void updateTime(long time) { + runOnUi( + () -> { + if (!hasLrc()) { + return; + } - mDrawableWidth = (int) getResources().getDimension(R.dimen.lrc_drawable_width); - mTimeTextWidth = (int) getResources().getDimension(R.dimen.lrc_time_width); + int line = findShowLine(time); + if (line != mCurrentLine) { + mCurrentLine = line; + if (!isShowTimeline) { + smoothScrollTo(line); + } else { + invalidate(); + } + } + }); + } - mLrcPaint.setAntiAlias(true); + /** + * 将歌词滚动到指定时间 + * + * @param time 指定的时间 + * @deprecated 请使用 {@link #updateTime(long)} 代替 + */ + @Deprecated + public void onDrag(long time) { + updateTime(time); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + initPlayDrawable(); + initEntryList(); + if (hasLrc()) { + smoothScrollTo(mCurrentLine, 0L); + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int centerY = getHeight() / 2; + + // 无歌词文件 + if (!hasLrc()) { + mLrcPaint.setColor(mCurrentTextColor); + @SuppressLint("DrawAllocation") + StaticLayout staticLayout = + new StaticLayout( + mDefaultLabel, + mLrcPaint, + (int) getLrcWidth(), + Layout.Alignment.ALIGN_CENTER, + 1f, + 0f, + false); + drawText(canvas, staticLayout, centerY); + return; + } + + int centerLine = getCenterLine(); + + if (isShowTimeline) { + mPlayDrawable.draw(canvas); + + mTimePaint.setColor(mTimelineColor); + canvas.drawLine(mTimeTextWidth, centerY, getWidth() - mTimeTextWidth, centerY, mTimePaint); + + mTimePaint.setColor(mTimeTextColor); + String timeText = LrcUtils.formatTime(mLrcEntryList.get(centerLine).getTime()); + float timeX = getWidth() - mTimeTextWidth / 2; + float timeY = centerY - (mTimeFontMetrics.descent + mTimeFontMetrics.ascent) / 2; + canvas.drawText(timeText, timeX, timeY, mTimePaint); + } + + canvas.translate(0, mOffset); + + float y = 0; + for (int i = 0; i < mLrcEntryList.size(); i++) { + if (i > 0) { + y += + ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + + mDividerHeight; + } + if (BuildConfig.DEBUG) { + // mLrcPaint.setTypeface(ResourcesCompat.getFont(getContext(), R.font.sans)); + } + if (i == mCurrentLine) { mLrcPaint.setTextSize(mCurrentTextSize); - mLrcPaint.setTextAlign(Paint.Align.LEFT); - mTimePaint.setAntiAlias(true); - mTimePaint.setTextSize(timeTextSize); - mTimePaint.setTextAlign(Paint.Align.CENTER); - //noinspection SuspiciousNameCombination - mTimePaint.setStrokeWidth(timelineHeight); - mTimePaint.setStrokeCap(Paint.Cap.ROUND); - mTimeFontMetrics = mTimePaint.getFontMetrics(); + mLrcPaint.setColor(mCurrentTextColor); + } else if (isShowTimeline && i == centerLine) { + mLrcPaint.setColor(mTimelineTextColor); + } else { + mLrcPaint.setTextSize(mNormalTextSize); + mLrcPaint.setColor(mNormalTextColor); + } + drawText(canvas, mLrcEntryList.get(i).getStaticLayout(), y); + } + } - mGestureDetector = new GestureDetector(getContext(), mSimpleOnGestureListener); - mGestureDetector.setIsLongpressEnabled(false); - mScroller = new Scroller(getContext()); + /** + * 画一行歌词 + * + * @param y 歌词中心 Y 坐标 + */ + private void drawText(Canvas canvas, StaticLayout staticLayout, float y) { + canvas.save(); + canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1)); + staticLayout.draw(canvas); + canvas.restore(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + isTouching = false; + if (hasLrc() && !isFling) { + adjustCenter(); + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); + } + } + return mGestureDetector.onTouchEvent(event); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + mOffset = mScroller.getCurrY(); + invalidate(); } - /** - * 设置非当前行歌词字体颜色 - */ - public void setNormalColor(int normalColor) { - mNormalTextColor = normalColor; - postInvalidate(); + if (isFling && mScroller.isFinished()) { + isFling = false; + if (hasLrc() && !isTouching) { + adjustCenter(); + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); + } + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(hideTimelineRunnable); + super.onDetachedFromWindow(); + } + + private void onLrcLoaded(List entryList) { + if (entryList != null && !entryList.isEmpty()) { + mLrcEntryList.addAll(entryList); } - /** - * 普通歌词文本字体大小 - */ - public void setNormalTextSize(float size) { - mNormalTextSize = size; + Collections.sort(mLrcEntryList); + + initEntryList(); + invalidate(); + } + + private void initPlayDrawable() { + int l = (mTimeTextWidth - mDrawableWidth) / 2; + int t = getHeight() / 2 - mDrawableWidth / 2; + int r = l + mDrawableWidth; + int b = t + mDrawableWidth; + mPlayDrawable.setBounds(l, t, r, b); + } + + private void initEntryList() { + if (!hasLrc() || getWidth() == 0) { + return; } - /** - * 当前歌词文本字体大小 - */ - public void setCurrentTextSize(float size) { - mCurrentTextSize = size; + for (LrcEntry lrcEntry : mLrcEntryList) { + lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity); } - /** - * 设置当前行歌词的字体颜色 - */ - public void setCurrentColor(int currentColor) { - mCurrentTextColor = currentColor; - postInvalidate(); - } + mOffset = getHeight() / 2; + } - /** - * 设置拖动歌词时选中歌词的字体颜色 - */ - public void setTimelineTextColor(int timelineTextColor) { - mTimelineTextColor = timelineTextColor; - postInvalidate(); - } + private void reset() { + endAnimation(); + mScroller.forceFinished(true); + isShowTimeline = false; + isTouching = false; + isFling = false; + removeCallbacks(hideTimelineRunnable); + mLrcEntryList.clear(); + mOffset = 0; + mCurrentLine = 0; + invalidate(); + } - /** - * 设置拖动歌词时时间线的颜色 - */ - public void setTimelineColor(int timelineColor) { - mTimelineColor = timelineColor; - postInvalidate(); - } + /** 将中心行微调至正中心 */ + private void adjustCenter() { + smoothScrollTo(getCenterLine(), ADJUST_DURATION); + } - /** - * 设置拖动歌词时右侧时间字体颜色 - */ - public void setTimeTextColor(int timeTextColor) { - mTimeTextColor = timeTextColor; - postInvalidate(); - } + /** 滚动到某一行 */ + private void smoothScrollTo(int line) { + smoothScrollTo(line, mAnimationDuration); + } - /** - * 设置歌词是否允许拖动 - * - * @param draggable 是否允许拖动 - * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null - */ - public void setDraggable(boolean draggable, OnPlayClickListener onPlayClickListener) { - if (draggable) { - if (onPlayClickListener == null) { - throw new IllegalArgumentException("if draggable == true, onPlayClickListener must not be null"); - } - mOnPlayClickListener = onPlayClickListener; - } else { - mOnPlayClickListener = null; - } - } + /** 滚动到某一行 */ + private void smoothScrollTo(int line, long duration) { + float offset = getOffset(line); + endAnimation(); - /** - * 设置播放按钮点击监听器 - * - * @param onPlayClickListener 如果为非 null ,则激活歌词拖动功能,否则将将禁用歌词拖动功能 - * @deprecated use {@link #setDraggable(boolean, OnPlayClickListener)} instead - */ - @Deprecated - public void setOnPlayClickListener(OnPlayClickListener onPlayClickListener) { - mOnPlayClickListener = onPlayClickListener; - } - - /** - * 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” - */ - public void setLabel(String label) { - runOnUi(() -> { - mDefaultLabel = label; - invalidate(); + mAnimator = ValueAnimator.ofFloat(mOffset, offset); + mAnimator.setDuration(duration); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.addUpdateListener( + animation -> { + mOffset = (float) animation.getAnimatedValue(); + invalidate(); }); + LrcUtils.resetDurationScale(); + mAnimator.start(); + } + + /** 结束滚动动画 */ + private void endAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.end(); + } + } + + /** 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) */ + private int findShowLine(long time) { + int left = 0; + int right = mLrcEntryList.size(); + while (left <= right) { + int middle = (left + right) / 2; + long middleTime = mLrcEntryList.get(middle).getTime(); + + if (time < middleTime) { + right = middle - 1; + } else { + if (middle + 1 >= mLrcEntryList.size() || time < mLrcEntryList.get(middle + 1).getTime()) { + return middle; + } + + left = middle + 1; + } } + return 0; + } + + /** 获取当前在视图中央的行数 */ + private int getCenterLine() { + int centerLine = 0; + float minDistance = Float.MAX_VALUE; + for (int i = 0; i < mLrcEntryList.size(); i++) { + if (Math.abs(mOffset - getOffset(i)) < minDistance) { + minDistance = Math.abs(mOffset - getOffset(i)); + centerLine = i; + } + } + return centerLine; + } + + /** 获取歌词距离视图顶部的距离 采用懒加载方式 */ + private float getOffset(int line) { + if (mLrcEntryList.get(line).getOffset() == Float.MIN_VALUE) { + float offset = getHeight() / 2; + for (int i = 1; i <= line; i++) { + offset -= + ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + + mDividerHeight; + } + mLrcEntryList.get(line).setOffset(offset); + } + + return mLrcEntryList.get(line).getOffset(); + } + + /** 获取歌词宽度 */ + private float getLrcWidth() { + return getWidth() - mLrcPadding * 2; + } + + /** 在主线程中运行 */ + private void runOnUi(Runnable r) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run(); + } else { + post(r); + } + } + + private Object getFlag() { + return mFlag; + } + + private void setFlag(Object flag) { + this.mFlag = flag; + } + + /** 播放按钮点击监听器,点击后应该跳转到指定播放位置 */ + public interface OnPlayClickListener { /** - * 加载歌词文件 + * 播放按钮被点击,应该跳转到指定播放位置 * - * @param lrcFile 歌词文件 + * @return 是否成功消费该事件,如果成功消费,则会更新UI */ - public void loadLrc(File lrcFile) { - loadLrc(lrcFile, null); - } - - /** - * 加载双语歌词文件,两种语言的歌词时间戳需要一致 - * - * @param mainLrcFile 第一种语言歌词文件 - * @param secondLrcFile 第二种语言歌词文件 - */ - public void loadLrc(File mainLrcFile, File secondLrcFile) { - runOnUi(() -> { - reset(); - - StringBuilder sb = new StringBuilder("file://"); - sb.append(mainLrcFile.getPath()); - if (secondLrcFile != null) { - sb.append("#").append(secondLrcFile.getPath()); - } - String flag = sb.toString(); - setFlag(flag); - new AsyncTask>() { - @Override - protected List doInBackground(File... params) { - return LrcUtils.parseLrc(params); - } - - @Override - protected void onPostExecute(List lrcEntries) { - if (getFlag() == flag) { - onLrcLoaded(lrcEntries); - setFlag(null); - } - } - }.execute(mainLrcFile, secondLrcFile); - }); - } - - /** - * 加载歌词文本 - * - * @param lrcText 歌词文本 - */ - public void loadLrc(String lrcText) { - loadLrc(lrcText, null); - } - - /** - * 加载双语歌词文本,两种语言的歌词时间戳需要一致 - * - * @param mainLrcText 第一种语言歌词文本 - * @param secondLrcText 第二种语言歌词文本 - */ - public void loadLrc(String mainLrcText, String secondLrcText) { - runOnUi(() -> { - reset(); - - StringBuilder sb = new StringBuilder("file://"); - sb.append(mainLrcText); - if (secondLrcText != null) { - sb.append("#").append(secondLrcText); - } - String flag = sb.toString(); - setFlag(flag); - new AsyncTask>() { - @Override - protected List doInBackground(String... params) { - return LrcUtils.parseLrc(params); - } - - @Override - protected void onPostExecute(List lrcEntries) { - if (getFlag() == flag) { - onLrcLoaded(lrcEntries); - setFlag(null); - } - } - }.execute(mainLrcText, secondLrcText); - }); - } - - /** - * 加载在线歌词,默认使用 utf-8 编码 - * - * @param lrcUrl 歌词文件的网络地址 - */ - public void loadLrcByUrl(String lrcUrl) { - loadLrcByUrl(lrcUrl, "utf-8"); - } - - /** - * 加载在线歌词 - * - * @param lrcUrl 歌词文件的网络地址 - * @param charset 编码格式 - */ - public void loadLrcByUrl(String lrcUrl, String charset) { - String flag = "url://" + lrcUrl; - setFlag(flag); - new AsyncTask() { - @Override - protected String doInBackground(String... params) { - return LrcUtils.getContentFromNetwork(params[0], params[1]); - } - - @Override - protected void onPostExecute(String lrcText) { - if (getFlag() == flag) { - loadLrc(lrcText); - } - } - }.execute(lrcUrl, charset); - } - - /** - * 歌词是否有效 - * - * @return true,如果歌词有效,否则false - */ - public boolean hasLrc() { - return !mLrcEntryList.isEmpty(); - } - - /** - * 刷新歌词 - * - * @param time 当前播放时间 - */ - public void updateTime(long time) { - runOnUi(() -> { - if (!hasLrc()) { - return; - } - - int line = findShowLine(time); - if (line != mCurrentLine) { - mCurrentLine = line; - if (!isShowTimeline) { - smoothScrollTo(line); - } else { - invalidate(); - } - } - }); - } - - /** - * 将歌词滚动到指定时间 - * - * @param time 指定的时间 - * @deprecated 请使用 {@link #updateTime(long)} 代替 - */ - @Deprecated - public void onDrag(long time) { - updateTime(time); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - initPlayDrawable(); - initEntryList(); - if (hasLrc()) { - smoothScrollTo(mCurrentLine, 0L); - } - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - int centerY = getHeight() / 2; - - // 无歌词文件 - if (!hasLrc()) { - mLrcPaint.setColor(mCurrentTextColor); - @SuppressLint("DrawAllocation") - StaticLayout staticLayout = new StaticLayout(mDefaultLabel, mLrcPaint, - (int) getLrcWidth(), Layout.Alignment.ALIGN_CENTER, 1f, 0f, false); - drawText(canvas, staticLayout, centerY); - return; - } - - int centerLine = getCenterLine(); - - if (isShowTimeline) { - mPlayDrawable.draw(canvas); - - mTimePaint.setColor(mTimelineColor); - canvas.drawLine(mTimeTextWidth, centerY, getWidth() - mTimeTextWidth, centerY, mTimePaint); - - mTimePaint.setColor(mTimeTextColor); - String timeText = LrcUtils.formatTime(mLrcEntryList.get(centerLine).getTime()); - float timeX = getWidth() - mTimeTextWidth / 2; - float timeY = centerY - (mTimeFontMetrics.descent + mTimeFontMetrics.ascent) / 2; - canvas.drawText(timeText, timeX, timeY, mTimePaint); - } - - canvas.translate(0, mOffset); - - float y = 0; - for (int i = 0; i < mLrcEntryList.size(); i++) { - if (i > 0) { - y += ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + mDividerHeight; - } - if (BuildConfig.DEBUG) { - //mLrcPaint.setTypeface(ResourcesCompat.getFont(getContext(), R.font.sans)); - } - if (i == mCurrentLine) { - mLrcPaint.setTextSize(mCurrentTextSize); - mLrcPaint.setColor(mCurrentTextColor); - } else if (isShowTimeline && i == centerLine) { - mLrcPaint.setColor(mTimelineTextColor); - } else { - mLrcPaint.setTextSize(mNormalTextSize); - mLrcPaint.setColor(mNormalTextColor); - } - drawText(canvas, mLrcEntryList.get(i).getStaticLayout(), y); - } - } - - /** - * 画一行歌词 - * - * @param y 歌词中心 Y 坐标 - */ - private void drawText(Canvas canvas, StaticLayout staticLayout, float y) { - canvas.save(); - canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1)); - staticLayout.draw(canvas); - canvas.restore(); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { - isTouching = false; - if (hasLrc() && !isFling) { - adjustCenter(); - postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); - } - } - return mGestureDetector.onTouchEvent(event); - } - - @Override - public void computeScroll() { - if (mScroller.computeScrollOffset()) { - mOffset = mScroller.getCurrY(); - invalidate(); - } - - if (isFling && mScroller.isFinished()) { - isFling = false; - if (hasLrc() && !isTouching) { - adjustCenter(); - postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); - } - } - } - - @Override - protected void onDetachedFromWindow() { - removeCallbacks(hideTimelineRunnable); - super.onDetachedFromWindow(); - } - - private void onLrcLoaded(List entryList) { - if (entryList != null && !entryList.isEmpty()) { - mLrcEntryList.addAll(entryList); - } - - Collections.sort(mLrcEntryList); - - initEntryList(); - invalidate(); - } - - private void initPlayDrawable() { - int l = (mTimeTextWidth - mDrawableWidth) / 2; - int t = getHeight() / 2 - mDrawableWidth / 2; - int r = l + mDrawableWidth; - int b = t + mDrawableWidth; - mPlayDrawable.setBounds(l, t, r, b); - } - - private void initEntryList() { - if (!hasLrc() || getWidth() == 0) { - return; - } - - for (LrcEntry lrcEntry : mLrcEntryList) { - lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity); - } - - mOffset = getHeight() / 2; - } - - private void reset() { - endAnimation(); - mScroller.forceFinished(true); - isShowTimeline = false; - isTouching = false; - isFling = false; - removeCallbacks(hideTimelineRunnable); - mLrcEntryList.clear(); - mOffset = 0; - mCurrentLine = 0; - invalidate(); - } - - /** - * 将中心行微调至正中心 - */ - private void adjustCenter() { - smoothScrollTo(getCenterLine(), ADJUST_DURATION); - } - - /** - * 滚动到某一行 - */ - private void smoothScrollTo(int line) { - smoothScrollTo(line, mAnimationDuration); - } - - /** - * 滚动到某一行 - */ - private void smoothScrollTo(int line, long duration) { - float offset = getOffset(line); - endAnimation(); - - mAnimator = ValueAnimator.ofFloat(mOffset, offset); - mAnimator.setDuration(duration); - mAnimator.setInterpolator(new LinearInterpolator()); - mAnimator.addUpdateListener(animation -> { - mOffset = (float) animation.getAnimatedValue(); - invalidate(); - }); - LrcUtils.resetDurationScale(); - mAnimator.start(); - } - - /** - * 结束滚动动画 - */ - private void endAnimation() { - if (mAnimator != null && mAnimator.isRunning()) { - mAnimator.end(); - } - } - - /** - * 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) - */ - private int findShowLine(long time) { - int left = 0; - int right = mLrcEntryList.size(); - while (left <= right) { - int middle = (left + right) / 2; - long middleTime = mLrcEntryList.get(middle).getTime(); - - if (time < middleTime) { - right = middle - 1; - } else { - if (middle + 1 >= mLrcEntryList.size() || time < mLrcEntryList.get(middle + 1).getTime()) { - return middle; - } - - left = middle + 1; - } - } - - return 0; - } - - /** - * 获取当前在视图中央的行数 - */ - private int getCenterLine() { - int centerLine = 0; - float minDistance = Float.MAX_VALUE; - for (int i = 0; i < mLrcEntryList.size(); i++) { - if (Math.abs(mOffset - getOffset(i)) < minDistance) { - minDistance = Math.abs(mOffset - getOffset(i)); - centerLine = i; - } - } - return centerLine; - } - - /** - * 获取歌词距离视图顶部的距离 - * 采用懒加载方式 - */ - private float getOffset(int line) { - if (mLrcEntryList.get(line).getOffset() == Float.MIN_VALUE) { - float offset = getHeight() / 2; - for (int i = 1; i <= line; i++) { - offset -= ((mLrcEntryList.get(i - 1).getHeight() + mLrcEntryList.get(i).getHeight()) >> 1) + mDividerHeight; - } - mLrcEntryList.get(line).setOffset(offset); - } - - return mLrcEntryList.get(line).getOffset(); - } - - /** - * 获取歌词宽度 - */ - private float getLrcWidth() { - return getWidth() - mLrcPadding * 2; - } - - /** - * 在主线程中运行 - */ - private void runOnUi(Runnable r) { - if (Looper.myLooper() == Looper.getMainLooper()) { - r.run(); - } else { - post(r); - } - } - - private Object getFlag() { - return mFlag; - } - - private void setFlag(Object flag) { - this.mFlag = flag; - } - - /** - * 播放按钮点击监听器,点击后应该跳转到指定播放位置 - */ - public interface OnPlayClickListener { - /** - * 播放按钮被点击,应该跳转到指定播放位置 - * - * @return 是否成功消费该事件,如果成功消费,则会更新UI - */ - boolean onPlayClick(long time); - } -} \ No newline at end of file + boolean onPlayClick(long time); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/CustomFragmentStatePagerAdapter.java b/app/src/main/java/code/name/monkey/retromusic/misc/CustomFragmentStatePagerAdapter.java index 31ce18746..2c40d3bd4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/misc/CustomFragmentStatePagerAdapter.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/CustomFragmentStatePagerAdapter.java @@ -19,219 +19,222 @@ import android.os.Parcelable; import android.util.Log; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentTransaction; import androidx.viewpager.widget.PagerAdapter; - import java.util.ArrayList; /** - * Implementation of {@link PagerAdapter} that - * uses a {@link Fragment} to manage each page. This class also handles - * saving and restoring of fragment's state. - *

- *

This version of the pager is more useful when there are a large number - * of pages, working more like a list view. When pages are not visible to - * the user, their entire fragment may be destroyed, only keeping the saved - * state of that fragment. This allows the pager to hold on to much less - * memory associated with each visited page as compared to - * {@link FragmentPagerAdapter} at the cost of potentially more overhead when - * switching between pages. - *

- *

When using FragmentPagerAdapter the host ViewPager must have a - * valid ID set.

- *

- *

Subclasses only need to implement {@link #getItem(int)} - * and {@link #getCount()} to have a working adapter. - *

- *

Here is an example implementation of a pager containing fragments of - * lists: - *

- * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java + * Implementation of {@link PagerAdapter} that uses a {@link Fragment} to manage each page. This + * class also handles saving and restoring of fragment's state. + * + *

+ * + *

This version of the pager is more useful when there are a large number of pages, working more + * like a list view. When pages are not visible to the user, their entire fragment may be destroyed, + * only keeping the saved state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to {@link FragmentPagerAdapter} at the cost + * of potentially more overhead when switching between pages. + * + *

+ * + *

When using FragmentPagerAdapter the host ViewPager must have a valid ID set. + * + *

+ * + *

Subclasses only need to implement {@link #getItem(int)} and {@link #getCount()} to have a + * working adapter. + * + *

+ * + *

Here is an example implementation of a pager containing fragments of lists: + * + *

{@sample + * development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java * complete} - *

+ * + *

+ * *

The R.layout.fragment_pager resource of the top-level fragment is: - *

- * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml - * complete} - *

- *

The R.layout.fragment_pager_list resource containing each - * individual fragment's layout is: - *

- * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml - * complete} + * + *

{@sample development/samples/Support13Demos/res/layout/fragment_pager.xml complete} + * + *

+ * + *

The R.layout.fragment_pager_list resource containing each individual fragment's + * layout is: + * + *

{@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml complete} */ public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter { - public static final String TAG = CustomFragmentStatePagerAdapter.class.getSimpleName(); - private static final boolean DEBUG = false; + public static final String TAG = CustomFragmentStatePagerAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; - private final FragmentManager mFragmentManager; - private FragmentTransaction mCurTransaction = null; + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; - private ArrayList mSavedState = new ArrayList(); - private ArrayList mFragments = new ArrayList(); - private Fragment mCurrentPrimaryItem = null; + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; - public CustomFragmentStatePagerAdapter(FragmentManager fm) { - mFragmentManager = fm; + public CustomFragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** Return the Fragment associated with a specified position. */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) {} + + @NonNull + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } } - /** - * Return the Fragment associated with a specified position. - */ - public abstract Fragment getItem(int position); - - @Override - public void startUpdate(ViewGroup container) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); } - @NonNull - @Override - public Object instantiateItem(ViewGroup container, int position) { - // If we already have this item instantiated, there is nothing - // to do. This can happen when we are restoring the entire pager - // from its saved state, where the fragment manager has already - // taken care of restoring the fragments we previously had instantiated. - if (mFragments.size() > position) { - Fragment f = mFragments.get(position); - if (f != null) { - return f; + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment) object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) + Log.v( + TAG, + "Removing item #" + position + ": f=" + object + " v=" + ((Fragment) object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment) object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment) object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i = 0; i < mFragments.size(); i++) { + Fragment f = mFragments.get(i); + if (f != null && f.isAdded()) { + if (state == null) { + state = new Bundle(); + } + String key = "f" + i; + mFragmentManager.putFragment(state, key, f); + } + } + return state; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + if (state != null) { + Bundle bundle = (Bundle) state; + bundle.setClassLoader(loader); + Parcelable[] fss = bundle.getParcelableArray("states"); + mSavedState.clear(); + mFragments.clear(); + if (fss != null) { + for (int i = 0; i < fss.length; i++) { + mSavedState.add((Fragment.SavedState) fss[i]); + } + } + Iterable keys = bundle.keySet(); + for (String key : keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); } + f.setMenuVisibility(false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } } - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - - Fragment fragment = getItem(position); - if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); - if (mSavedState.size() > position) { - Fragment.SavedState fss = mSavedState.get(position); - if (fss != null) { - fragment.setInitialSavedState(fss); - } - } - while (mFragments.size() <= position) { - mFragments.add(null); - } - fragment.setMenuVisibility(false); - fragment.setUserVisibleHint(false); - mFragments.set(position, fragment); - mCurTransaction.add(container.getId(), fragment); - - return fragment; + } } + } - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - Fragment fragment = (Fragment) object; - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object - + " v=" + ((Fragment) object).getView()); - while (mSavedState.size() <= position) { - mSavedState.add(null); - } - mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); - mFragments.set(position, null); - - mCurTransaction.remove(fragment); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - Fragment fragment = (Fragment) object; - if (fragment != mCurrentPrimaryItem) { - if (mCurrentPrimaryItem != null) { - mCurrentPrimaryItem.setMenuVisibility(false); - mCurrentPrimaryItem.setUserVisibleHint(false); - } - if (fragment != null) { - fragment.setMenuVisibility(true); - fragment.setUserVisibleHint(true); - } - mCurrentPrimaryItem = fragment; - } - } - - @Override - public void finishUpdate(ViewGroup container) { - if (mCurTransaction != null) { - mCurTransaction.commitAllowingStateLoss(); - mCurTransaction = null; - mFragmentManager.executePendingTransactions(); - } - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return ((Fragment) object).getView() == view; - } - - @Override - public Parcelable saveState() { - Bundle state = null; - if (mSavedState.size() > 0) { - state = new Bundle(); - Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; - mSavedState.toArray(fss); - state.putParcelableArray("states", fss); - } - for (int i = 0; i < mFragments.size(); i++) { - Fragment f = mFragments.get(i); - if (f != null && f.isAdded()) { - if (state == null) { - state = new Bundle(); - } - String key = "f" + i; - mFragmentManager.putFragment(state, key, f); - } - } - return state; - } - - @Override - public void restoreState(Parcelable state, ClassLoader loader) { - if (state != null) { - Bundle bundle = (Bundle) state; - bundle.setClassLoader(loader); - Parcelable[] fss = bundle.getParcelableArray("states"); - mSavedState.clear(); - mFragments.clear(); - if (fss != null) { - for (int i = 0; i < fss.length; i++) { - mSavedState.add((Fragment.SavedState) fss[i]); - } - } - Iterable keys = bundle.keySet(); - for (String key : keys) { - if (key.startsWith("f")) { - int index = Integer.parseInt(key.substring(1)); - Fragment f = mFragmentManager.getFragment(bundle, key); - if (f != null) { - while (mFragments.size() <= index) { - mFragments.add(null); - } - f.setMenuVisibility(false); - mFragments.set(index, f); - } else { - Log.w(TAG, "Bad fragment at key " + key); - } - } - } - } - } - - public Fragment getFragment(int position) { - if (position < mFragments.size() && position >= 0) { - return mFragments.get(position); - } - return null; + public Fragment getFragment(int position) { + if (position < mFragments.size() && position >= 0) { + return mFragments.get(position); } + return null; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/DialogAsyncTask.java b/app/src/main/java/code/name/monkey/retromusic/misc/DialogAsyncTask.java index 88e387622..d83cc1623 100644 --- a/app/src/main/java/code/name/monkey/retromusic/misc/DialogAsyncTask.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/DialogAsyncTask.java @@ -17,90 +17,86 @@ package code.name.monkey.retromusic.misc; import android.app.Dialog; import android.content.Context; import android.os.Handler; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.lang.ref.WeakReference; +public abstract class DialogAsyncTask + extends WeakContextAsyncTask { + private final int delay; -public abstract class DialogAsyncTask extends WeakContextAsyncTask { - private final int delay; + private WeakReference

dialogWeakReference; - private WeakReference dialogWeakReference; + private boolean supposedToBeDismissed; - private boolean supposedToBeDismissed; + public DialogAsyncTask(Context context) { + this(context, 0); + } - public DialogAsyncTask(Context context) { - this(context, 0); + public DialogAsyncTask(Context context, int showDelay) { + super(context); + this.delay = showDelay; + dialogWeakReference = new WeakReference<>(null); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (delay > 0) { + new Handler().postDelayed(this::initAndShowDialog, delay); + } else { + initAndShowDialog(); } + } - public DialogAsyncTask(Context context, int showDelay) { - super(context); - this.delay = showDelay; - dialogWeakReference = new WeakReference<>(null); + private void initAndShowDialog() { + Context context = getContext(); + if (!supposedToBeDismissed && context != null) { + Dialog dialog = createDialog(context); + dialogWeakReference = new WeakReference<>(dialog); + dialog.show(); } + } - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (delay > 0) { - new Handler().postDelayed(this::initAndShowDialog, delay); - } else { - initAndShowDialog(); - } + @SuppressWarnings("unchecked") + @Override + protected void onProgressUpdate(Progress... values) { + super.onProgressUpdate(values); + Dialog dialog = getDialog(); + if (dialog != null) { + onProgressUpdate(dialog, values); } + } - private void initAndShowDialog() { - Context context = getContext(); - if (!supposedToBeDismissed && context != null) { - Dialog dialog = createDialog(context); - dialogWeakReference = new WeakReference<>(dialog); - dialog.show(); - } + @SuppressWarnings("unchecked") + protected void onProgressUpdate(@NonNull Dialog dialog, Progress... values) {} + + @Nullable + protected Dialog getDialog() { + return dialogWeakReference.get(); + } + + @Override + protected void onCancelled(Result result) { + super.onCancelled(result); + tryToDismiss(); + } + + @Override + protected void onPostExecute(Result result) { + super.onPostExecute(result); + tryToDismiss(); + } + + private void tryToDismiss() { + supposedToBeDismissed = true; + try { + Dialog dialog = getDialog(); + if (dialog != null) dialog.dismiss(); + } catch (Exception e) { + e.printStackTrace(); } + } - @SuppressWarnings("unchecked") - @Override - protected void onProgressUpdate(Progress... values) { - super.onProgressUpdate(values); - Dialog dialog = getDialog(); - if (dialog != null) { - onProgressUpdate(dialog, values); - } - } - - @SuppressWarnings("unchecked") - protected void onProgressUpdate(@NonNull Dialog dialog, Progress... values) { - } - - @Nullable - protected Dialog getDialog() { - return dialogWeakReference.get(); - } - - @Override - protected void onCancelled(Result result) { - super.onCancelled(result); - tryToDismiss(); - } - - @Override - protected void onPostExecute(Result result) { - super.onPostExecute(result); - tryToDismiss(); - } - - private void tryToDismiss() { - supposedToBeDismissed = true; - try { - Dialog dialog = getDialog(); - if (dialog != null) - dialog.dismiss(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - protected abstract Dialog createDialog(@NonNull Context context); + protected abstract Dialog createDialog(@NonNull Context context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/GenericFileProvider.java b/app/src/main/java/code/name/monkey/retromusic/misc/GenericFileProvider.java index 1e87ad8e4..61a0a80a5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/misc/GenericFileProvider.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/GenericFileProvider.java @@ -16,5 +16,4 @@ package code.name.monkey.retromusic.misc; import androidx.core.content.FileProvider; -public class GenericFileProvider extends FileProvider { -} \ No newline at end of file +public class GenericFileProvider extends FileProvider {} diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/LagTracker.java b/app/src/main/java/code/name/monkey/retromusic/misc/LagTracker.java index 48baf8d0e..402e2b389 100755 --- a/app/src/main/java/code/name/monkey/retromusic/misc/LagTracker.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/LagTracker.java @@ -15,62 +15,71 @@ package code.name.monkey.retromusic.misc; import android.util.Log; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class LagTracker { - private static Map mMap; - private static LagTracker mSingleton; - private boolean mEnabled = true; + private static Map mMap; + private static LagTracker mSingleton; + private boolean mEnabled = true; - private LagTracker() { - mMap = new HashMap(); - } + private LagTracker() { + mMap = new HashMap(); + } - public static LagTracker get() { - if (mSingleton == null) { - mSingleton = new LagTracker(); - } - return mSingleton; + public static LagTracker get() { + if (mSingleton == null) { + mSingleton = new LagTracker(); } + return mSingleton; + } - private void print(String str, long j) { - long toMillis = TimeUnit.NANOSECONDS.toMillis(j); - Log.d("LagTracker", "[" + str + " completed in]: " + j + " ns (" + toMillis + "ms, " + TimeUnit.NANOSECONDS.toSeconds(j) + "s)"); - } + private void print(String str, long j) { + long toMillis = TimeUnit.NANOSECONDS.toMillis(j); + Log.d( + "LagTracker", + "[" + + str + + " completed in]: " + + j + + " ns (" + + toMillis + + "ms, " + + TimeUnit.NANOSECONDS.toSeconds(j) + + "s)"); + } - public LagTracker disable() { - this.mEnabled = false; - return this; - } + public LagTracker disable() { + this.mEnabled = false; + return this; + } - public LagTracker enable() { - this.mEnabled = true; - return this; - } + public LagTracker enable() { + this.mEnabled = true; + return this; + } - public void end(String str) { - long nanoTime = System.nanoTime(); - if (this.mEnabled) { - if (mMap.containsKey(str)) { - print(str, nanoTime - mMap.get(str).longValue()); - mMap.remove(str); - return; - } - throw new IllegalStateException("No start time found for " + str); - } else if (!mMap.isEmpty()) { - mMap.clear(); - } + public void end(String str) { + long nanoTime = System.nanoTime(); + if (this.mEnabled) { + if (mMap.containsKey(str)) { + print(str, nanoTime - mMap.get(str).longValue()); + mMap.remove(str); + return; + } + throw new IllegalStateException("No start time found for " + str); + } else if (!mMap.isEmpty()) { + mMap.clear(); } + } - public void start(String str) { - long nanoTime = System.nanoTime(); - if (this.mEnabled) { - mMap.put(str, Long.valueOf(nanoTime)); - } else if (!mMap.isEmpty()) { - mMap.clear(); - } + public void start(String str) { + long nanoTime = System.nanoTime(); + if (this.mEnabled) { + mMap.put(str, Long.valueOf(nanoTime)); + } else if (!mMap.isEmpty()) { + mMap.clear(); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java b/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java index 84aa17c1f..03a45cac2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java @@ -19,49 +19,49 @@ import android.app.Activity; import android.media.MediaScannerConnection; import android.net.Uri; import android.widget.Toast; - +import code.name.monkey.retromusic.R; import java.lang.ref.WeakReference; -import code.name.monkey.retromusic.R; +/** @author Karim Abou Zeid (kabouzeid) */ +public class UpdateToastMediaScannerCompletionListener + implements MediaScannerConnection.OnScanCompletedListener { -/** - * @author Karim Abou Zeid (kabouzeid) - */ -public class UpdateToastMediaScannerCompletionListener implements MediaScannerConnection.OnScanCompletedListener { + private final WeakReference activityWeakReference; - private final WeakReference activityWeakReference; + private final String couldNotScanFiles; + private final String scannedFiles; + private final String[] toBeScanned; + private int failed = 0; + private int scanned = 0; + private Toast toast; - private final String couldNotScanFiles; - private final String scannedFiles; - private final String[] toBeScanned; - private int failed = 0; - private int scanned = 0; - private Toast toast; + @SuppressLint("ShowToast") + public UpdateToastMediaScannerCompletionListener(Activity activity, String[] toBeScanned) { + this.toBeScanned = toBeScanned; + scannedFiles = activity.getString(R.string.scanned_files); + couldNotScanFiles = activity.getString(R.string.could_not_scan_files); + toast = Toast.makeText(activity.getApplicationContext(), "", Toast.LENGTH_SHORT); + activityWeakReference = new WeakReference<>(activity); + } - @SuppressLint("ShowToast") - public UpdateToastMediaScannerCompletionListener(Activity activity, String[] toBeScanned) { - this.toBeScanned = toBeScanned; - scannedFiles = activity.getString(R.string.scanned_files); - couldNotScanFiles = activity.getString(R.string.could_not_scan_files); - toast = Toast.makeText(activity.getApplicationContext(), "", Toast.LENGTH_SHORT); - activityWeakReference = new WeakReference<>(activity); - } - - @Override - public void onScanCompleted(final String path, final Uri uri) { - Activity activity = activityWeakReference.get(); - if (activity != null) { - activity.runOnUiThread(() -> { - if (uri == null) { - failed++; - } else { - scanned++; - } - String text = " " + String.format(scannedFiles, scanned, toBeScanned.length) + (failed > 0 ? " " - + String.format(couldNotScanFiles, failed) : ""); - toast.setText(text); - toast.show(); - }); - } + @Override + public void onScanCompleted(final String path, final Uri uri) { + Activity activity = activityWeakReference.get(); + if (activity != null) { + activity.runOnUiThread( + () -> { + if (uri == null) { + failed++; + } else { + scanned++; + } + String text = + " " + + String.format(scannedFiles, scanned, toBeScanned.length) + + (failed > 0 ? " " + String.format(couldNotScanFiles, failed) : ""); + toast.setText(text); + toast.show(); + }); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/AbsSynchronizedLyrics.java b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/AbsSynchronizedLyrics.java index a3a5f0b80..f939003e9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/AbsSynchronizedLyrics.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/AbsSynchronizedLyrics.java @@ -18,54 +18,55 @@ import android.util.SparseArray; public abstract class AbsSynchronizedLyrics extends Lyrics { - private static final int TIME_OFFSET_MS = 500; // time adjustment to display line before it actually starts + private static final int TIME_OFFSET_MS = + 500; // time adjustment to display line before it actually starts - protected final SparseArray lines = new SparseArray<>(); + protected final SparseArray lines = new SparseArray<>(); - protected int offset = 0; + protected int offset = 0; - public String getLine(int time) { - time += offset + AbsSynchronizedLyrics.TIME_OFFSET_MS; + public String getLine(int time) { + time += offset + AbsSynchronizedLyrics.TIME_OFFSET_MS; - int lastLineTime = lines.keyAt(0); + int lastLineTime = lines.keyAt(0); - for (int i = 0; i < lines.size(); i++) { - int lineTime = lines.keyAt(i); + for (int i = 0; i < lines.size(); i++) { + int lineTime = lines.keyAt(i); - if (time >= lineTime) { - lastLineTime = lineTime; - } else { - break; - } - } - - return lines.get(lastLineTime); + if (time >= lineTime) { + lastLineTime = lineTime; + } else { + break; + } } - @Override - public String getText() { - parse(false); + return lines.get(lastLineTime); + } - if (valid) { - StringBuilder sb = new StringBuilder(); + @Override + public String getText() { + parse(false); - for (int i = 0; i < lines.size(); i++) { - String line = lines.valueAt(i); - sb.append(line).append("\r\n"); - } + if (valid) { + StringBuilder sb = new StringBuilder(); - return sb.toString().trim().replaceAll("(\r?\n){3,}", "\r\n\r\n"); - } + for (int i = 0; i < lines.size(); i++) { + String line = lines.valueAt(i); + sb.append(line).append("\r\n"); + } - return super.getText(); + return sb.toString().trim().replaceAll("(\r?\n){3,}", "\r\n\r\n"); } - public boolean isSynchronized() { - return true; - } + return super.getText(); + } - public boolean isValid() { - parse(true); - return valid; - } + public boolean isSynchronized() { + return true; + } + + public boolean isValid() { + parse(true); + return valid; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/Lyrics.java b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/Lyrics.java index 32bbfd4aa..dad81ff6a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/Lyrics.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/Lyrics.java @@ -14,74 +14,72 @@ package code.name.monkey.retromusic.model.lyrics; - -import java.util.ArrayList; - import code.name.monkey.retromusic.model.Song; +import java.util.ArrayList; public class Lyrics { - private static final ArrayList> FORMATS = new ArrayList<>(); + private static final ArrayList> FORMATS = new ArrayList<>(); - static { - Lyrics.FORMATS.add(SynchronizedLyricsLRC.class); - } + static { + Lyrics.FORMATS.add(SynchronizedLyricsLRC.class); + } - public String data; - public Song song; - protected boolean parsed = false; - protected boolean valid = false; + public String data; + public Song song; + protected boolean parsed = false; + protected boolean valid = false; - public static boolean isSynchronized(String data) { - for (Class format : Lyrics.FORMATS) { - try { - Lyrics lyrics = format.newInstance().setData(null, data); - if (lyrics.isValid()) { - return true; - } - } catch (Exception e) { - e.printStackTrace(); - } + public static boolean isSynchronized(String data) { + for (Class format : Lyrics.FORMATS) { + try { + Lyrics lyrics = format.newInstance().setData(null, data); + if (lyrics.isValid()) { + return true; } - return false; + } catch (Exception e) { + e.printStackTrace(); + } } + return false; + } - public static Lyrics parse(Song song, String data) { - for (Class format : Lyrics.FORMATS) { - try { - Lyrics lyrics = format.newInstance().setData(song, data); - if (lyrics.isValid()) { - return lyrics.parse(false); - } - } catch (Exception e) { - e.printStackTrace(); - } + public static Lyrics parse(Song song, String data) { + for (Class format : Lyrics.FORMATS) { + try { + Lyrics lyrics = format.newInstance().setData(song, data); + if (lyrics.isValid()) { + return lyrics.parse(false); } - return new Lyrics().setData(song, data).parse(false); + } catch (Exception e) { + e.printStackTrace(); + } } + return new Lyrics().setData(song, data).parse(false); + } - public String getText() { - return this.data.trim().replaceAll("(\r?\n){3,}", "\r\n\r\n"); - } + public String getText() { + return this.data.trim().replaceAll("(\r?\n){3,}", "\r\n\r\n"); + } - public boolean isSynchronized() { - return false; - } + public boolean isSynchronized() { + return false; + } - public boolean isValid() { - this.parse(true); - return this.valid; - } + public boolean isValid() { + this.parse(true); + return this.valid; + } - public Lyrics parse(boolean check) { - this.valid = true; - this.parsed = true; - return this; - } + public Lyrics parse(boolean check) { + this.valid = true; + this.parsed = true; + return this; + } - public Lyrics setData(Song song, String data) { - this.song = song; - this.data = data; - return this; - } + public Lyrics setData(Song song, String data) { + this.song = song; + this.data = data; + return this; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/SynchronizedLyricsLRC.java b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/SynchronizedLyricsLRC.java index 02b8d1c42..409184913 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/lyrics/SynchronizedLyricsLRC.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/lyrics/SynchronizedLyricsLRC.java @@ -19,72 +19,73 @@ import java.util.regex.Pattern; class SynchronizedLyricsLRC extends AbsSynchronizedLyrics { - private static final Pattern LRC_LINE_PATTERN = Pattern.compile("((?:\\[.*?\\])+)(.*)"); + private static final Pattern LRC_LINE_PATTERN = Pattern.compile("((?:\\[.*?\\])+)(.*)"); - private static final Pattern LRC_TIME_PATTERN = Pattern.compile("\\[(\\d+):(\\d{2}(?:\\.\\d+)?)\\]"); + private static final Pattern LRC_TIME_PATTERN = + Pattern.compile("\\[(\\d+):(\\d{2}(?:\\.\\d+)?)\\]"); - private static final Pattern LRC_ATTRIBUTE_PATTERN = Pattern.compile("\\[(\\D+):(.+)\\]"); + private static final Pattern LRC_ATTRIBUTE_PATTERN = Pattern.compile("\\[(\\D+):(.+)\\]"); - private static final float LRC_SECONDS_TO_MS_MULTIPLIER = 1000f; + private static final float LRC_SECONDS_TO_MS_MULTIPLIER = 1000f; - private static final int LRC_MINUTES_TO_MS_MULTIPLIER = 60000; + private static final int LRC_MINUTES_TO_MS_MULTIPLIER = 60000; - @Override - public SynchronizedLyricsLRC parse(boolean check) { - if (this.parsed || this.data == null || this.data.isEmpty()) { - return this; - } - - String[] lines = this.data.split("\r?\n"); - - for (String line : lines) { - line = line.trim(); - if (line.isEmpty()) { - continue; - } - - Matcher attrMatcher = SynchronizedLyricsLRC.LRC_ATTRIBUTE_PATTERN.matcher(line); - if (attrMatcher.find()) { - try { - String attr = attrMatcher.group(1).toLowerCase().trim(); - String value = attrMatcher.group(2).toLowerCase().trim(); - if ("offset".equals(attr)) { - this.offset = Integer.parseInt(value); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } else { - Matcher matcher = SynchronizedLyricsLRC.LRC_LINE_PATTERN.matcher(line); - if (matcher.find()) { - String time = matcher.group(1); - String text = matcher.group(2); - - Matcher timeMatcher = SynchronizedLyricsLRC.LRC_TIME_PATTERN.matcher(time); - while (timeMatcher.find()) { - int m = 0; - float s = 0f; - try { - m = Integer.parseInt(timeMatcher.group(1)); - s = Float.parseFloat(timeMatcher.group(2)); - } catch (NumberFormatException ex) { - ex.printStackTrace(); - } - int ms = (int) (s * LRC_SECONDS_TO_MS_MULTIPLIER) + m * LRC_MINUTES_TO_MS_MULTIPLIER; - - this.valid = true; - if (check) { - return this; - } - - this.lines.append(ms, text); - } - } - } - } - - this.parsed = true; - - return this; + @Override + public SynchronizedLyricsLRC parse(boolean check) { + if (this.parsed || this.data == null || this.data.isEmpty()) { + return this; } + + String[] lines = this.data.split("\r?\n"); + + for (String line : lines) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + Matcher attrMatcher = SynchronizedLyricsLRC.LRC_ATTRIBUTE_PATTERN.matcher(line); + if (attrMatcher.find()) { + try { + String attr = attrMatcher.group(1).toLowerCase().trim(); + String value = attrMatcher.group(2).toLowerCase().trim(); + if ("offset".equals(attr)) { + this.offset = Integer.parseInt(value); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } else { + Matcher matcher = SynchronizedLyricsLRC.LRC_LINE_PATTERN.matcher(line); + if (matcher.find()) { + String time = matcher.group(1); + String text = matcher.group(2); + + Matcher timeMatcher = SynchronizedLyricsLRC.LRC_TIME_PATTERN.matcher(time); + while (timeMatcher.find()) { + int m = 0; + float s = 0f; + try { + m = Integer.parseInt(timeMatcher.group(1)); + s = Float.parseFloat(timeMatcher.group(2)); + } catch (NumberFormatException ex) { + ex.printStackTrace(); + } + int ms = (int) (s * LRC_SECONDS_TO_MS_MULTIPLIER) + m * LRC_MINUTES_TO_MS_MULTIPLIER; + + this.valid = true; + if (check) { + return this; + } + + this.lines.append(ms, text); + } + } + } + } + + this.parsed = true; + + return this; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmAlbum.java b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmAlbum.java index 779a10b7a..b912503c8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmAlbum.java +++ b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmAlbum.java @@ -16,158 +16,144 @@ package code.name.monkey.retromusic.network.model; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - import java.util.ArrayList; import java.util.List; public class LastFmAlbum { - @Expose - private Album album; + @Expose private Album album; - public Album getAlbum() { - return album; + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + public static class Album { + + @Expose public String listeners; + @Expose public String playcount; + @Expose private List image = new ArrayList<>(); + @Expose private String name; + @Expose private Tags tags; + @Expose private Wiki wiki; + + public List getImage() { + return image; } - public void setAlbum(Album album) { - this.album = album; + public void setImage(List image) { + this.image = image; } - public static class Album { - - @Expose - public String listeners; - @Expose - public String playcount; - @Expose - private List image = new ArrayList<>(); - @Expose - private String name; - @Expose - private Tags tags; - @Expose - private Wiki wiki; - - public List getImage() { - return image; - } - - public void setImage(List image) { - this.image = image; - } - - public String getListeners() { - return listeners; - } - - public void setListeners(final String listeners) { - this.listeners = listeners; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getPlaycount() { - return playcount; - } - - public void setPlaycount(final String playcount) { - this.playcount = playcount; - } - - public Tags getTags() { - return tags; - } - - public Wiki getWiki() { - return wiki; - } - - public void setWiki(Wiki wiki) { - this.wiki = wiki; - } - - public static class Image { - - @SerializedName("#text") - @Expose - private String Text; - - @Expose - private String size; - - public String getSize() { - return size; - } - - public void setSize(String size) { - this.size = size; - } - - public String getText() { - return Text; - } - - public void setText(String Text) { - this.Text = Text; - } - } - - public class Tags { - - @Expose - private List tag = null; - - public List getTag() { - return tag; - } - } - - public class Tag { - - @Expose - private String name; - - @Expose - private String url; - - public String getName() { - return name; - } - - public String getUrl() { - return url; - } - } - - public class Wiki { - - @Expose - private String content; - - @Expose - private String published; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public String getPublished() { - return published; - } - - public void setPublished(final String published) { - this.published = published; - } - } + public String getListeners() { + return listeners; } + + public void setListeners(final String listeners) { + this.listeners = listeners; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getPlaycount() { + return playcount; + } + + public void setPlaycount(final String playcount) { + this.playcount = playcount; + } + + public Tags getTags() { + return tags; + } + + public Wiki getWiki() { + return wiki; + } + + public void setWiki(Wiki wiki) { + this.wiki = wiki; + } + + public static class Image { + + @SerializedName("#text") + @Expose + private String Text; + + @Expose private String size; + + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + public String getText() { + return Text; + } + + public void setText(String Text) { + this.Text = Text; + } + } + + public class Tags { + + @Expose private List tag = null; + + public List getTag() { + return tag; + } + } + + public class Tag { + + @Expose private String name; + + @Expose private String url; + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + } + + public class Wiki { + + @Expose private String content; + + @Expose private String published; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getPublished() { + return published; + } + + public void setPublished(final String published) { + this.published = published; + } + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmArtist.java b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmArtist.java index de593cd7c..0f91b5e43 100644 --- a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmArtist.java +++ b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmArtist.java @@ -16,111 +16,102 @@ package code.name.monkey.retromusic.network.model; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - import java.util.ArrayList; import java.util.List; public class LastFmArtist { - @Expose - private Artist artist; + @Expose private Artist artist; - public Artist getArtist() { - return artist; + public Artist getArtist() { + return artist; + } + + public void setArtist(Artist artist) { + this.artist = artist; + } + + public static class Artist { + + @Expose public Stats stats; + @Expose private Bio bio; + @Expose private List image = new ArrayList<>(); + + public Bio getBio() { + return bio; } - public void setArtist(Artist artist) { - this.artist = artist; + public void setBio(Bio bio) { + this.bio = bio; } - public static class Artist { - - @Expose - public Stats stats; - @Expose - private Bio bio; - @Expose - private List image = new ArrayList<>(); - - public Bio getBio() { - return bio; - } - - public void setBio(Bio bio) { - this.bio = bio; - } - - public List getImage() { - return image; - } - - public void setImage(List image) { - this.image = image; - } - - public static class Image { - - @SerializedName("#text") - @Expose - private String Text; - - @Expose - private String size; - - public String getSize() { - return size; - } - - public void setSize(String size) { - this.size = size; - } - - public String getText() { - return Text; - } - - public void setText(String Text) { - this.Text = Text; - } - } - - public static class Stats { - - @Expose - public String listeners; - - @Expose - public String playcount; - - public String getListeners() { - return listeners; - } - - public void setListeners(final String listeners) { - this.listeners = listeners; - } - - public String getPlaycount() { - return playcount; - } - - public void setPlaycount(final String playcount) { - this.playcount = playcount; - } - } - - public class Bio { - - @Expose - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - } + public List getImage() { + return image; } + + public void setImage(List image) { + this.image = image; + } + + public static class Image { + + @SerializedName("#text") + @Expose + private String Text; + + @Expose private String size; + + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + public String getText() { + return Text; + } + + public void setText(String Text) { + this.Text = Text; + } + } + + public static class Stats { + + @Expose public String listeners; + + @Expose public String playcount; + + public String getListeners() { + return listeners; + } + + public void setListeners(final String listeners) { + this.listeners = listeners; + } + + public String getPlaycount() { + return playcount; + } + + public void setPlaycount(final String playcount) { + this.playcount = playcount; + } + } + + public class Bio { + + @Expose private String content; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmTrack.java b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmTrack.java index fcb9d71aa..ee8848a38 100644 --- a/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmTrack.java +++ b/app/src/main/java/code/name/monkey/retromusic/network/model/LastFmTrack.java @@ -16,173 +16,157 @@ package code.name.monkey.retromusic.network.model; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; - import java.util.List; -/** - * Created by hemanths on 15/06/17. - */ - +/** Created by hemanths on 15/06/17. */ public class LastFmTrack { + @Expose private Track track; + + public Track getTrack() { + return track; + } + + public void setTrack(Track track) { + this.track = track; + } + + public static class Track { + @SerializedName("name") @Expose - private Track track; + private String name; - public Track getTrack() { - return track; + @Expose private Album album; + @Expose private Wiki wiki; + @Expose private Toptags toptags; + @Expose private Artist artist; + + public Album getAlbum() { + return album; } - public void setTrack(Track track) { - this.track = track; + public Wiki getWiki() { + return wiki; } - public static class Track { - @SerializedName("name") - @Expose - private String name; - @Expose - private Album album; - @Expose - private Wiki wiki; - @Expose - private Toptags toptags; - @Expose - private Artist artist; + public String getName() { + return name; + } - public Album getAlbum() { - return album; - } + public Toptags getToptags() { + return toptags; + } - public Wiki getWiki() { - return wiki; - } + public static class Artist { + + @Expose private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static class Wiki { + @Expose private String published; + + public String getPublished() { + return published; + } + + public void setPublished(String published) { + this.published = published; + } + } + + public static class Toptags { + @Expose private List tag = null; + + public List getTag() { + return tag; + } + + public static class Tag { + @Expose private String name; public String getName() { - return name; - } - - public Toptags getToptags() { - return toptags; - } - - public static class Artist { - - @Expose - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static class Wiki { - @Expose - private String published; - - public String getPublished() { - return published; - } - - public void setPublished(String published) { - this.published = published; - } - } - - public static class Toptags { - @Expose - private List tag = null; - - - public List getTag() { - return tag; - } - - public static class Tag { - @Expose - private String name; - - public String getName() { - return name; - } - } - } - - public static class Album { - @Expose - private String artist; - @Expose - private List image = null; - @Expose - private String title; - @SerializedName("@attr") - @Expose - private Attr attr; - - public Attr getAttr() { - return attr; - } - - public void setAttr(Attr attr) { - this.attr = attr; - } - - public String getArtist() { - return artist; - } - - public void setArtist(String artist) { - this.artist = artist; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getImage() { - return image; - } - - public void setImage(List image) { - this.image = image; - } - - public static class Attr { - @Expose - private String position; - - public String getPosition() { - return position; - } - - public void setPosition(String position) { - this.position = position; - } - } - - public class Image { - - @SerializedName("#text") - @Expose - private String text; - @Expose - private String size; - - public String getSize() { - return size; - } - - public String getText() { - return text; - } - } + return name; } + } } + + public static class Album { + @Expose private String artist; + @Expose private List image = null; + @Expose private String title; + + @SerializedName("@attr") + @Expose + private Attr attr; + + public Attr getAttr() { + return attr; + } + + public void setAttr(Attr attr) { + this.attr = attr; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getImage() { + return image; + } + + public void setImage(List image) { + this.image = image; + } + + public static class Attr { + @Expose private String position; + + public String getPosition() { + return position; + } + + public void setPosition(String position) { + this.position = position; + } + } + + public class Image { + + @SerializedName("#text") + @Expose + private String text; + + @Expose private String size; + + public String getSize() { + return size; + } + + public String getText() { + return text; + } + } + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java index bb775b93b..ee4486a40 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.providers; +import static code.name.monkey.retromusic.service.MusicService.MEDIA_STORE_CHANGED; + import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -21,150 +23,164 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; - import androidx.annotation.NonNull; - +import code.name.monkey.retromusic.util.FileUtil; +import code.name.monkey.retromusic.util.PreferenceUtil; import java.io.File; import java.util.ArrayList; -import code.name.monkey.retromusic.util.FileUtil; -import code.name.monkey.retromusic.util.PreferenceUtil; - -import static code.name.monkey.retromusic.service.MusicService.MEDIA_STORE_CHANGED; - public class BlacklistStore extends SQLiteOpenHelper { - public static final String DATABASE_NAME = "blacklist.db"; - private static final int VERSION = 2; - private static BlacklistStore sInstance = null; - private Context context; + public static final String DATABASE_NAME = "blacklist.db"; + private static final int VERSION = 2; + private static BlacklistStore sInstance = null; + private Context context; - public BlacklistStore(final Context context) { - super(context, DATABASE_NAME, null, VERSION); - this.context = context; + public BlacklistStore(final Context context) { + super(context, DATABASE_NAME, null, VERSION); + this.context = context; + } + + @NonNull + public static synchronized BlacklistStore getInstance(@NonNull final Context context) { + if (sInstance == null) { + sInstance = new BlacklistStore(context.getApplicationContext()); + if (!PreferenceUtil.INSTANCE.isInitializedBlacklist()) { + // blacklisted by default + sInstance.addPathImpl( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_ALARMS)); + sInstance.addPathImpl( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_NOTIFICATIONS)); + sInstance.addPathImpl( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES)); + + PreferenceUtil.INSTANCE.setInitializedBlacklist(true); + } + } + return sInstance; + } + + @Override + public void onCreate(@NonNull final SQLiteDatabase db) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS " + + BlacklistStoreColumns.NAME + + " (" + + BlacklistStoreColumns.PATH + + " STRING NOT NULL);"); + } + + @Override + public void onUpgrade( + @NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + BlacklistStoreColumns.NAME); + onCreate(db); + } + + @Override + public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + BlacklistStoreColumns.NAME); + onCreate(db); + } + + public void addPath(File file) { + addPathImpl(file); + notifyMediaStoreChanged(); + } + + private void addPathImpl(File file) { + if (file == null || contains(file)) { + return; + } + String path = FileUtil.safeGetCanonicalPath(file); + + final SQLiteDatabase database = getWritableDatabase(); + database.beginTransaction(); + + try { + // add the entry + final ContentValues values = new ContentValues(1); + values.put(BlacklistStoreColumns.PATH, path); + database.insert(BlacklistStoreColumns.NAME, null, values); + + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + public boolean contains(File file) { + if (file == null) { + return false; + } + String path = FileUtil.safeGetCanonicalPath(file); + + final SQLiteDatabase database = getReadableDatabase(); + Cursor cursor = + database.query( + BlacklistStoreColumns.NAME, + new String[] {BlacklistStoreColumns.PATH}, + BlacklistStoreColumns.PATH + "=?", + new String[] {path}, + null, + null, + null, + null); + + boolean containsPath = cursor != null && cursor.moveToFirst(); + if (cursor != null) { + cursor.close(); + } + return containsPath; + } + + public void removePath(File file) { + final SQLiteDatabase database = getWritableDatabase(); + String path = FileUtil.safeGetCanonicalPath(file); + + database.delete( + BlacklistStoreColumns.NAME, BlacklistStoreColumns.PATH + "=?", new String[] {path}); + + notifyMediaStoreChanged(); + } + + public void clear() { + final SQLiteDatabase database = getWritableDatabase(); + database.delete(BlacklistStoreColumns.NAME, null, null); + + notifyMediaStoreChanged(); + } + + private void notifyMediaStoreChanged() { + context.sendBroadcast(new Intent(MEDIA_STORE_CHANGED)); + } + + @NonNull + public ArrayList getPaths() { + Cursor cursor = + getReadableDatabase() + .query( + BlacklistStoreColumns.NAME, + new String[] {BlacklistStoreColumns.PATH}, + null, + null, + null, + null, + null); + + ArrayList paths = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + paths.add(cursor.getString(0)); + } while (cursor.moveToNext()); } - @NonNull - public static synchronized BlacklistStore getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new BlacklistStore(context.getApplicationContext()); - if (!PreferenceUtil.INSTANCE.isInitializedBlacklist()) { - // blacklisted by default - sInstance.addPathImpl(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_ALARMS)); - sInstance.addPathImpl(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_NOTIFICATIONS)); - sInstance.addPathImpl(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES)); + if (cursor != null) cursor.close(); + return paths; + } - PreferenceUtil.INSTANCE.setInitializedBlacklist(true); - } - } - return sInstance; - } + public interface BlacklistStoreColumns { + String NAME = "blacklist"; - @Override - public void onCreate(@NonNull final SQLiteDatabase db) { - db.execSQL("CREATE TABLE IF NOT EXISTS " + BlacklistStoreColumns.NAME + " (" + BlacklistStoreColumns.PATH + " STRING NOT NULL);"); - } - - @Override - public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + BlacklistStoreColumns.NAME); - onCreate(db); - } - - @Override - public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + BlacklistStoreColumns.NAME); - onCreate(db); - } - - public void addPath(File file) { - addPathImpl(file); - notifyMediaStoreChanged(); - } - - private void addPathImpl(File file) { - if (file == null || contains(file)) { - return; - } - String path = FileUtil.safeGetCanonicalPath(file); - - final SQLiteDatabase database = getWritableDatabase(); - database.beginTransaction(); - - try { - // add the entry - final ContentValues values = new ContentValues(1); - values.put(BlacklistStoreColumns.PATH, path); - database.insert(BlacklistStoreColumns.NAME, null, values); - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - } - - public boolean contains(File file) { - if (file == null) { - return false; - } - String path = FileUtil.safeGetCanonicalPath(file); - - final SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(BlacklistStoreColumns.NAME, - new String[]{BlacklistStoreColumns.PATH}, - BlacklistStoreColumns.PATH + "=?", - new String[]{path}, - null, null, null, null); - - boolean containsPath = cursor != null && cursor.moveToFirst(); - if (cursor != null) { - cursor.close(); - } - return containsPath; - } - - public void removePath(File file) { - final SQLiteDatabase database = getWritableDatabase(); - String path = FileUtil.safeGetCanonicalPath(file); - - database.delete(BlacklistStoreColumns.NAME, - BlacklistStoreColumns.PATH + "=?", - new String[]{path}); - - notifyMediaStoreChanged(); - } - - public void clear() { - final SQLiteDatabase database = getWritableDatabase(); - database.delete(BlacklistStoreColumns.NAME, null, null); - - notifyMediaStoreChanged(); - } - - private void notifyMediaStoreChanged() { - context.sendBroadcast(new Intent(MEDIA_STORE_CHANGED)); - } - - @NonNull - public ArrayList getPaths() { - Cursor cursor = getReadableDatabase().query(BlacklistStoreColumns.NAME, - new String[]{BlacklistStoreColumns.PATH}, - null, null, null, null, null); - - ArrayList paths = new ArrayList<>(); - if (cursor != null && cursor.moveToFirst()) { - do { - paths.add(cursor.getString(0)); - } while (cursor.moveToNext()); - } - - if (cursor != null) - cursor.close(); - return paths; - } - - public interface BlacklistStoreColumns { - String NAME = "blacklist"; - - String PATH = "path"; - } -} \ No newline at end of file + String PATH = "path"; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/HistoryStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/HistoryStore.java index e981aead8..996bb57a3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/HistoryStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/HistoryStore.java @@ -19,148 +19,169 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; public class HistoryStore extends SQLiteOpenHelper { - public static final String DATABASE_NAME = "history.db"; - private static final int MAX_ITEMS_IN_DB = 100; - private static final int VERSION = 1; - @Nullable - private static HistoryStore sInstance = null; + public static final String DATABASE_NAME = "history.db"; + private static final int MAX_ITEMS_IN_DB = 100; + private static final int VERSION = 1; + @Nullable private static HistoryStore sInstance = null; - public HistoryStore(final Context context) { - super(context, DATABASE_NAME, null, VERSION); + public HistoryStore(final Context context) { + super(context, DATABASE_NAME, null, VERSION); + } + + @NonNull + public static synchronized HistoryStore getInstance(@NonNull final Context context) { + if (sInstance == null) { + sInstance = new HistoryStore(context.getApplicationContext()); + } + return sInstance; + } + + @Override + public void onCreate(@NonNull final SQLiteDatabase db) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS " + + RecentStoreColumns.NAME + + " (" + + RecentStoreColumns.ID + + " LONG NOT NULL," + + RecentStoreColumns.TIME_PLAYED + + " LONG NOT NULL);"); + } + + @Override + public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME); + onCreate(db); + } + + @Override + public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME); + onCreate(db); + } + + public void addSongId(final long songId) { + if (songId == -1) { + return; } - @NonNull - public static synchronized HistoryStore getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new HistoryStore(context.getApplicationContext()); + final SQLiteDatabase database = getWritableDatabase(); + database.beginTransaction(); + + try { + // remove previous entries + removeSongId(songId); + + // add the entry + final ContentValues values = new ContentValues(2); + values.put(RecentStoreColumns.ID, songId); + values.put(RecentStoreColumns.TIME_PLAYED, System.currentTimeMillis()); + database.insert(RecentStoreColumns.NAME, null, values); + + // if our db is too large, delete the extra items + Cursor oldest = null; + try { + oldest = + database.query( + RecentStoreColumns.NAME, + new String[] {RecentStoreColumns.TIME_PLAYED}, + null, + null, + null, + null, + RecentStoreColumns.TIME_PLAYED + " ASC"); + + if (oldest != null && oldest.getCount() > MAX_ITEMS_IN_DB) { + oldest.moveToPosition(oldest.getCount() - MAX_ITEMS_IN_DB); + long timeOfRecordToKeep = oldest.getLong(0); + + database.delete( + RecentStoreColumns.NAME, + RecentStoreColumns.TIME_PLAYED + " < ?", + new String[] {String.valueOf(timeOfRecordToKeep)}); } - return sInstance; - } - - @Override - public void onCreate(@NonNull final SQLiteDatabase db) { - db.execSQL("CREATE TABLE IF NOT EXISTS " + RecentStoreColumns.NAME + " (" - + RecentStoreColumns.ID + " LONG NOT NULL," + RecentStoreColumns.TIME_PLAYED - + " LONG NOT NULL);"); - } - - @Override - public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME); - onCreate(db); - } - - @Override - public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + RecentStoreColumns.NAME); - onCreate(db); - } - - public void addSongId(final long songId) { - if (songId == -1) { - return; - } - - final SQLiteDatabase database = getWritableDatabase(); - database.beginTransaction(); - - try { - // remove previous entries - removeSongId(songId); - - // add the entry - final ContentValues values = new ContentValues(2); - values.put(RecentStoreColumns.ID, songId); - values.put(RecentStoreColumns.TIME_PLAYED, System.currentTimeMillis()); - database.insert(RecentStoreColumns.NAME, null, values); - - // if our db is too large, delete the extra items - Cursor oldest = null; - try { - oldest = database.query(RecentStoreColumns.NAME, - new String[]{RecentStoreColumns.TIME_PLAYED}, null, null, null, null, - RecentStoreColumns.TIME_PLAYED + " ASC"); - - if (oldest != null && oldest.getCount() > MAX_ITEMS_IN_DB) { - oldest.moveToPosition(oldest.getCount() - MAX_ITEMS_IN_DB); - long timeOfRecordToKeep = oldest.getLong(0); - - database.delete(RecentStoreColumns.NAME, - RecentStoreColumns.TIME_PLAYED + " < ?", - new String[]{String.valueOf(timeOfRecordToKeep)}); - - } - } finally { - if (oldest != null) { - oldest.close(); - } - } - } finally { - database.setTransactionSuccessful(); - database.endTransaction(); + } finally { + if (oldest != null) { + oldest.close(); } + } + } finally { + database.setTransactionSuccessful(); + database.endTransaction(); } + } - public void removeSongId(final long songId) { - final SQLiteDatabase database = getWritableDatabase(); - database.delete(RecentStoreColumns.NAME, RecentStoreColumns.ID + " = ?", new String[]{ - String.valueOf(songId) - }); + public void removeSongId(final long songId) { + final SQLiteDatabase database = getWritableDatabase(); + database.delete( + RecentStoreColumns.NAME, + RecentStoreColumns.ID + " = ?", + new String[] {String.valueOf(songId)}); + } + public void clear() { + final SQLiteDatabase database = getWritableDatabase(); + database.delete(RecentStoreColumns.NAME, null, null); + } + + public boolean contains(long id) { + final SQLiteDatabase database = getReadableDatabase(); + Cursor cursor = + database.query( + RecentStoreColumns.NAME, + new String[] {RecentStoreColumns.ID}, + RecentStoreColumns.ID + "=?", + new String[] {String.valueOf(id)}, + null, + null, + null, + null); + + boolean containsId = cursor != null && cursor.moveToFirst(); + if (cursor != null) { + cursor.close(); } + return containsId; + } - public void clear() { - final SQLiteDatabase database = getWritableDatabase(); - database.delete(RecentStoreColumns.NAME, null, null); - } + public Cursor queryRecentIds() { + final SQLiteDatabase database = getReadableDatabase(); + return database.query( + RecentStoreColumns.NAME, + new String[] {RecentStoreColumns.ID}, + null, + null, + null, + null, + RecentStoreColumns.TIME_PLAYED + " DESC"); + } - public boolean contains(long id) { - final SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(RecentStoreColumns.NAME, - new String[]{RecentStoreColumns.ID}, - RecentStoreColumns.ID + "=?", - new String[]{String.valueOf(id)}, - null, null, null, null); + public Cursor queryRecentIds(long cutoff) { + final boolean noCutoffTime = (cutoff == 0); + final boolean reverseOrder = (cutoff < 0); + if (reverseOrder) cutoff = -cutoff; - boolean containsId = cursor != null && cursor.moveToFirst(); - if (cursor != null) { - cursor.close(); - } - return containsId; - } + final SQLiteDatabase database = getReadableDatabase(); - public Cursor queryRecentIds() { - final SQLiteDatabase database = getReadableDatabase(); - return database.query(RecentStoreColumns.NAME, - new String[]{RecentStoreColumns.ID}, null, null, null, null, - RecentStoreColumns.TIME_PLAYED + " DESC"); - } + return database.query( + RecentStoreColumns.NAME, + new String[] {RecentStoreColumns.ID}, + noCutoffTime ? null : RecentStoreColumns.TIME_PLAYED + (reverseOrder ? "?"), + noCutoffTime ? null : new String[] {String.valueOf(cutoff)}, + null, + null, + RecentStoreColumns.TIME_PLAYED + (reverseOrder ? " ASC" : " DESC")); + } - public Cursor queryRecentIds(long cutoff) { - final boolean noCutoffTime = (cutoff == 0); - final boolean reverseOrder = (cutoff < 0); - if (reverseOrder) cutoff = -cutoff; + public interface RecentStoreColumns { + String NAME = "recent_history"; - final SQLiteDatabase database = getReadableDatabase(); + String ID = "song_id"; - return database.query(RecentStoreColumns.NAME, - new String[]{RecentStoreColumns.ID}, - noCutoffTime ? null : RecentStoreColumns.TIME_PLAYED + (reverseOrder ? "?"), - noCutoffTime ? null : new String[]{String.valueOf(cutoff)}, - null, null, - RecentStoreColumns.TIME_PLAYED + (reverseOrder ? " ASC" : " DESC")); - } - - public interface RecentStoreColumns { - String NAME = "recent_history"; - - String ID = "song_id"; - - String TIME_PLAYED = "time_played"; - } + String TIME_PLAYED = "time_played"; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java index ee2e040eb..1d1934a6a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java @@ -20,195 +20,190 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.provider.MediaStore.Audio.AudioColumns; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.util.List; - import code.name.monkey.retromusic.App; import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.repository.RealSongRepository; +import java.util.List; /** * @author Andrew Neal, modified for Phonograph by Karim Abou Zeid - *

- * This keeps track of the music playback and history state of the playback service + *

This keeps track of the music playback and history state of the playback service */ public class MusicPlaybackQueueStore extends SQLiteOpenHelper { - public static final String DATABASE_NAME = "music_playback_state.db"; + public static final String DATABASE_NAME = "music_playback_state.db"; - public static final String PLAYING_QUEUE_TABLE_NAME = "playing_queue"; + public static final String PLAYING_QUEUE_TABLE_NAME = "playing_queue"; - public static final String ORIGINAL_PLAYING_QUEUE_TABLE_NAME = "original_playing_queue"; + public static final String ORIGINAL_PLAYING_QUEUE_TABLE_NAME = "original_playing_queue"; - private static final int VERSION = 12; + private static final int VERSION = 12; - @Nullable - private static MusicPlaybackQueueStore sInstance = null; + @Nullable private static MusicPlaybackQueueStore sInstance = null; - /** - * Constructor of MusicPlaybackState - * - * @param context The {@link Context} to use - */ - public MusicPlaybackQueueStore(final @NonNull Context context) { - super(context, DATABASE_NAME, null, VERSION); + /** + * Constructor of MusicPlaybackState + * + * @param context The {@link Context} to use + */ + public MusicPlaybackQueueStore(final @NonNull Context context) { + super(context, DATABASE_NAME, null, VERSION); + } + + /** + * @param context The {@link Context} to use + * @return A new instance of this class. + */ + @NonNull + public static synchronized MusicPlaybackQueueStore getInstance(@NonNull final Context context) { + if (sInstance == null) { + sInstance = new MusicPlaybackQueueStore(context.getApplicationContext()); + } + return sInstance; + } + + @Override + public void onCreate(@NonNull final SQLiteDatabase db) { + createTable(db, PLAYING_QUEUE_TABLE_NAME); + createTable(db, ORIGINAL_PLAYING_QUEUE_TABLE_NAME); + } + + @NonNull + public List getSavedOriginalPlayingQueue() { + return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME); + } + + @NonNull + public List getSavedPlayingQueue() { + return getQueue(PLAYING_QUEUE_TABLE_NAME); + } + + @Override + public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // If we ever have downgrade, drop the table to be safe + db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); + onCreate(db); + } + + @Override + public void onUpgrade( + @NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { + // not necessary yet + db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); + onCreate(db); + } + + public synchronized void saveQueues( + @NonNull final List playingQueue, @NonNull final List originalPlayingQueue) { + saveQueue(PLAYING_QUEUE_TABLE_NAME, playingQueue); + saveQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME, originalPlayingQueue); + } + + private void createTable(@NonNull final SQLiteDatabase db, final String tableName) { + //noinspection StringBufferReplaceableByString + StringBuilder builder = new StringBuilder(); + builder.append("CREATE TABLE IF NOT EXISTS "); + builder.append(tableName); + builder.append("("); + + builder.append(BaseColumns._ID); + builder.append(" INT NOT NULL,"); + + builder.append(AudioColumns.TITLE); + builder.append(" STRING NOT NULL,"); + + builder.append(AudioColumns.TRACK); + builder.append(" INT NOT NULL,"); + + builder.append(AudioColumns.YEAR); + builder.append(" INT NOT NULL,"); + + builder.append(AudioColumns.DURATION); + builder.append(" LONG NOT NULL,"); + + builder.append(AudioColumns.DATA); + builder.append(" STRING NOT NULL,"); + + builder.append(AudioColumns.DATE_MODIFIED); + builder.append(" LONG NOT NULL,"); + + builder.append(AudioColumns.ALBUM_ID); + builder.append(" INT NOT NULL,"); + + builder.append(AudioColumns.ALBUM); + builder.append(" STRING NOT NULL,"); + + builder.append(AudioColumns.ARTIST_ID); + builder.append(" INT NOT NULL,"); + + builder.append(AudioColumns.ARTIST); + builder.append(" STRING NOT NULL,"); + + builder.append(AudioColumns.COMPOSER); + builder.append(" STRING,"); + + builder.append("album_artist"); + builder.append(" STRING);"); + + db.execSQL(builder.toString()); + } + + @NonNull + private List getQueue(@NonNull final String tableName) { + Cursor cursor = getReadableDatabase().query(tableName, null, null, null, null, null, null); + return new RealSongRepository(App.Companion.getContext()).songs(cursor); + } + + /** + * Clears the existing database and saves the queue into the db so that when the app is restarted, + * the tracks you were listening to is restored + * + * @param queue the queue to save + */ + private synchronized void saveQueue(final String tableName, @NonNull final List queue) { + final SQLiteDatabase database = getWritableDatabase(); + database.beginTransaction(); + + try { + database.delete(tableName, null, null); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); } - /** - * @param context The {@link Context} to use - * @return A new instance of this class. - */ - @NonNull - public static synchronized MusicPlaybackQueueStore getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new MusicPlaybackQueueStore(context.getApplicationContext()); - } - return sInstance; - } - - @Override - public void onCreate(@NonNull final SQLiteDatabase db) { - createTable(db, PLAYING_QUEUE_TABLE_NAME); - createTable(db, ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - } - - @NonNull - public List getSavedOriginalPlayingQueue() { - return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - } - - @NonNull - public List getSavedPlayingQueue() { - return getQueue(PLAYING_QUEUE_TABLE_NAME); - } - - @Override - public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - // If we ever have downgrade, drop the table to be safe - db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - onCreate(db); - } - - @Override - public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { - // not necessary yet - db.execSQL("DROP TABLE IF EXISTS " + PLAYING_QUEUE_TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + ORIGINAL_PLAYING_QUEUE_TABLE_NAME); - onCreate(db); - } - - public synchronized void saveQueues(@NonNull final List playingQueue, - @NonNull final List originalPlayingQueue) { - saveQueue(PLAYING_QUEUE_TABLE_NAME, playingQueue); - saveQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME, originalPlayingQueue); - } - - private void createTable(@NonNull final SQLiteDatabase db, final String tableName) { - //noinspection StringBufferReplaceableByString - StringBuilder builder = new StringBuilder(); - builder.append("CREATE TABLE IF NOT EXISTS "); - builder.append(tableName); - builder.append("("); - - builder.append(BaseColumns._ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.TITLE); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.TRACK); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.YEAR); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.DURATION); - builder.append(" LONG NOT NULL,"); - - builder.append(AudioColumns.DATA); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.DATE_MODIFIED); - builder.append(" LONG NOT NULL,"); - - builder.append(AudioColumns.ALBUM_ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.ALBUM); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.ARTIST_ID); - builder.append(" INT NOT NULL,"); - - builder.append(AudioColumns.ARTIST); - builder.append(" STRING NOT NULL,"); - - builder.append(AudioColumns.COMPOSER); - builder.append(" STRING,"); - - builder.append("album_artist"); - builder.append(" STRING);"); - - db.execSQL(builder.toString()); - } - - @NonNull - private List getQueue(@NonNull final String tableName) { - Cursor cursor = getReadableDatabase().query(tableName, null, - null, null, null, null, null); - return new RealSongRepository(App.Companion.getContext()).songs(cursor); - } - - /** - * Clears the existing database and saves the queue into the db so that when the - * app is restarted, the tracks you were listening to is restored - * - * @param queue the queue to save - */ - private synchronized void saveQueue(final String tableName, @NonNull final List queue) { - final SQLiteDatabase database = getWritableDatabase(); - database.beginTransaction(); - - try { - database.delete(tableName, null, null); - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - final int NUM_PROCESS = 20; - int position = 0; - while (position < queue.size()) { - database.beginTransaction(); - try { - for (int i = position; i < queue.size() && i < position + NUM_PROCESS; i++) { - Song song = queue.get(i); - ContentValues values = new ContentValues(4); - - values.put(BaseColumns._ID, song.getId()); - values.put(AudioColumns.TITLE, song.getTitle()); - values.put(AudioColumns.TRACK, song.getTrackNumber()); - values.put(AudioColumns.YEAR, song.getYear()); - values.put(AudioColumns.DURATION, song.getDuration()); - values.put(AudioColumns.DATA, song.getData()); - values.put(AudioColumns.DATE_MODIFIED, song.getDateModified()); - values.put(AudioColumns.ALBUM_ID, song.getAlbumId()); - values.put(AudioColumns.ALBUM, song.getAlbumName()); - values.put(AudioColumns.ARTIST_ID, song.getArtistId()); - values.put(AudioColumns.ARTIST, song.getArtistName()); - values.put(AudioColumns.COMPOSER, song.getComposer()); - - database.insert(tableName, null, values); - } - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - position += NUM_PROCESS; - } + final int NUM_PROCESS = 20; + int position = 0; + while (position < queue.size()) { + database.beginTransaction(); + try { + for (int i = position; i < queue.size() && i < position + NUM_PROCESS; i++) { + Song song = queue.get(i); + ContentValues values = new ContentValues(4); + + values.put(BaseColumns._ID, song.getId()); + values.put(AudioColumns.TITLE, song.getTitle()); + values.put(AudioColumns.TRACK, song.getTrackNumber()); + values.put(AudioColumns.YEAR, song.getYear()); + values.put(AudioColumns.DURATION, song.getDuration()); + values.put(AudioColumns.DATA, song.getData()); + values.put(AudioColumns.DATE_MODIFIED, song.getDateModified()); + values.put(AudioColumns.ALBUM_ID, song.getAlbumId()); + values.put(AudioColumns.ALBUM, song.getAlbumName()); + values.put(AudioColumns.ARTIST_ID, song.getArtistId()); + values.put(AudioColumns.ARTIST, song.getArtistName()); + values.put(AudioColumns.COMPOSER, song.getComposer()); + + database.insert(tableName, null, values); } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + position += NUM_PROCESS; + } } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/SongPlayCountStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/SongPlayCountStore.java index 4701c0622..c19903f9a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/SongPlayCountStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/SongPlayCountStore.java @@ -21,383 +21,400 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** - * This database tracks the number of play counts for an individual song. This is used to drive - * the top played tracks as well as the playlist images + * This database tracks the number of play counts for an individual song. This is used to drive the + * top played tracks as well as the playlist images */ public class SongPlayCountStore extends SQLiteOpenHelper { - public static final String DATABASE_NAME = "song_play_count.db"; - private static final int VERSION = 3; - // how many weeks worth of playback to track - private static final int NUM_WEEKS = 52; - @Nullable - private static SongPlayCountStore sInstance = null; - // interpolator curve applied for measuring the curve - @NonNull - private static Interpolator sInterpolator = new AccelerateInterpolator(1.5f); - // how high to multiply the interpolation curve - @SuppressWarnings("FieldCanBeLocal") - private static int INTERPOLATOR_HEIGHT = 50; + public static final String DATABASE_NAME = "song_play_count.db"; + private static final int VERSION = 3; + // how many weeks worth of playback to track + private static final int NUM_WEEKS = 52; + @Nullable private static SongPlayCountStore sInstance = null; + // interpolator curve applied for measuring the curve + @NonNull private static Interpolator sInterpolator = new AccelerateInterpolator(1.5f); + // how high to multiply the interpolation curve + @SuppressWarnings("FieldCanBeLocal") + private static int INTERPOLATOR_HEIGHT = 50; - // how high the base value is. The ratio of the Height to Base is what really matters - @SuppressWarnings("FieldCanBeLocal") - private static int INTERPOLATOR_BASE = 25; + // how high the base value is. The ratio of the Height to Base is what really matters + @SuppressWarnings("FieldCanBeLocal") + private static int INTERPOLATOR_BASE = 25; - @SuppressWarnings("FieldCanBeLocal") - private static int ONE_WEEK_IN_MS = 1000 * 60 * 60 * 24 * 7; + @SuppressWarnings("FieldCanBeLocal") + private static int ONE_WEEK_IN_MS = 1000 * 60 * 60 * 24 * 7; - @NonNull - private static String WHERE_ID_EQUALS = SongPlayCountColumns.ID + "=?"; + @NonNull private static String WHERE_ID_EQUALS = SongPlayCountColumns.ID + "=?"; - // number of weeks since epoch time - private int mNumberOfWeeksSinceEpoch; + // number of weeks since epoch time + private int mNumberOfWeeksSinceEpoch; - // used to track if we've walked through the db and updated all the rows - private boolean mDatabaseUpdated; + // used to track if we've walked through the db and updated all the rows + private boolean mDatabaseUpdated; - public SongPlayCountStore(final Context context) { - super(context, DATABASE_NAME, null, VERSION); + public SongPlayCountStore(final Context context) { + super(context, DATABASE_NAME, null, VERSION); - long msSinceEpoch = System.currentTimeMillis(); - mNumberOfWeeksSinceEpoch = (int) (msSinceEpoch / ONE_WEEK_IN_MS); + long msSinceEpoch = System.currentTimeMillis(); + mNumberOfWeeksSinceEpoch = (int) (msSinceEpoch / ONE_WEEK_IN_MS); - mDatabaseUpdated = false; + mDatabaseUpdated = false; + } + + /** + * @param context The {@link Context} to use + * @return A new instance of this class. + */ + @NonNull + public static synchronized SongPlayCountStore getInstance(@NonNull final Context context) { + if (sInstance == null) { + sInstance = new SongPlayCountStore(context.getApplicationContext()); + } + return sInstance; + } + + /** + * Calculates the score of the song given the play counts + * + * @param playCounts an array of the # of times a song has been played for each week where + * playCounts[N] is the # of times it was played N weeks ago + * @return the score + */ + private static float calculateScore(@Nullable final int[] playCounts) { + if (playCounts == null) { + return 0; } - /** - * @param context The {@link Context} to use - * @return A new instance of this class. - */ - @NonNull - public static synchronized SongPlayCountStore getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new SongPlayCountStore(context.getApplicationContext()); + float score = 0; + for (int i = 0; i < Math.min(playCounts.length, NUM_WEEKS); i++) { + score += playCounts[i] * getScoreMultiplierForWeek(i); + } + + return score; + } + + /** + * Gets the column name for each week # + * + * @param week number + * @return the column name + */ + @NonNull + private static String getColumnNameForWeek(final int week) { + return SongPlayCountColumns.WEEK_PLAY_COUNT + week; + } + + /** + * Gets the score multiplier for each week + * + * @param week number + * @return the multiplier to apply + */ + private static float getScoreMultiplierForWeek(final int week) { + return sInterpolator.getInterpolation(1 - (week / (float) NUM_WEEKS)) * INTERPOLATOR_HEIGHT + + INTERPOLATOR_BASE; + } + + /** + * For some performance gain, return a static value for the column index for a week WARNING: This + * function assumes you have selected all columns for it to work + * + * @param week number + * @return column index of that week + */ + private static int getColumnIndexForWeek(final int week) { + // ID, followed by the weeks columns + return 1 + week; + } + + @Override + public void onCreate(@NonNull final SQLiteDatabase db) { + // create the play count table + // WARNING: If you change the order of these columns + // please update getColumnIndexForWeek + StringBuilder builder = new StringBuilder(); + builder.append("CREATE TABLE IF NOT EXISTS "); + builder.append(SongPlayCountColumns.NAME); + builder.append("("); + builder.append(SongPlayCountColumns.ID); + builder.append(" INT UNIQUE,"); + + for (int i = 0; i < NUM_WEEKS; i++) { + builder.append(getColumnNameForWeek(i)); + builder.append(" INT DEFAULT 0,"); + } + + builder.append(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX); + builder.append(" INT NOT NULL,"); + + builder.append(SongPlayCountColumns.PLAY_COUNT_SCORE); + builder.append(" REAL DEFAULT 0);"); + + db.execSQL(builder.toString()); + } + + @Override + public void onUpgrade( + @NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME); + onCreate(db); + } + + @Override + public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // If we ever have downgrade, drop the table to be safe + db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME); + onCreate(db); + } + + /** + * Increases the play count of a song by 1 + * + * @param songId The song id to increase the play count + */ + public void bumpPlayCount(final long songId) { + if (songId == -1) { + return; + } + + final SQLiteDatabase database = getWritableDatabase(); + updateExistingRow(database, songId, true); + } + + /** + * This creates a new entry that indicates a song has been played once as well as its score + * + * @param database a write able database + * @param songId the id of the track + */ + private void createNewPlayedEntry(@NonNull final SQLiteDatabase database, final long songId) { + // no row exists, create a new one + float newScore = getScoreMultiplierForWeek(0); + int newPlayCount = 1; + + final ContentValues values = new ContentValues(3); + values.put(SongPlayCountColumns.ID, songId); + values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, newScore); + values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch); + values.put(getColumnNameForWeek(0), newPlayCount); + + database.insert(SongPlayCountColumns.NAME, null, values); + } + + /** + * This function will take a song entry and update it to the latest week and increase the count + * for the current week by 1 if necessary + * + * @param database a writeable database + * @param id the id of the track to bump + * @param bumpCount whether to bump the current's week play count by 1 and adjust the score + */ + private void updateExistingRow( + @NonNull final SQLiteDatabase database, final long id, boolean bumpCount) { + String stringId = String.valueOf(id); + + // begin the transaction + database.beginTransaction(); + + // get the cursor of this content inside the transaction + final Cursor cursor = + database.query( + SongPlayCountColumns.NAME, + null, + WHERE_ID_EQUALS, + new String[] {stringId}, + null, + null, + null); + + // if we have a result + if (cursor != null && cursor.moveToFirst()) { + // figure how many weeks since we last updated + int lastUpdatedIndex = cursor.getColumnIndex(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX); + int lastUpdatedWeek = cursor.getInt(lastUpdatedIndex); + int weekDiff = mNumberOfWeeksSinceEpoch - lastUpdatedWeek; + + // if it's more than the number of weeks we track, delete it and create a new entry + if (Math.abs(weekDiff) >= NUM_WEEKS) { + // this entry needs to be dropped since it is too outdated + deleteEntry(database, stringId); + if (bumpCount) { + createNewPlayedEntry(database, id); } - return sInstance; - } + } else if (weekDiff != 0) { + // else, shift the weeks + int[] playCounts = new int[NUM_WEEKS]; - /** - * Calculates the score of the song given the play counts - * - * @param playCounts an array of the # of times a song has been played for each week - * where playCounts[N] is the # of times it was played N weeks ago - * @return the score - */ - private static float calculateScore(@Nullable final int[] playCounts) { - if (playCounts == null) { - return 0; + if (weekDiff > 0) { + // time is shifted forwards + for (int i = 0; i < NUM_WEEKS - weekDiff; i++) { + playCounts[i + weekDiff] = cursor.getInt(getColumnIndexForWeek(i)); + } + } else if (weekDiff < 0) { + // time is shifted backwards (by user) - nor typical behavior but we + // will still handle it + + // since weekDiff is -ve, NUM_WEEKS + weekDiff is the real # of weeks we have to + // transfer. Then we transfer the old week i - weekDiff to week i + // for example if the user shifted back 2 weeks, ie -2, then for 0 to + // NUM_WEEKS + (-2) we set the new week i = old week i - (-2) or i+2 + for (int i = 0; i < NUM_WEEKS + weekDiff; i++) { + playCounts[i] = cursor.getInt(getColumnIndexForWeek(i - weekDiff)); + } } - float score = 0; - for (int i = 0; i < Math.min(playCounts.length, NUM_WEEKS); i++) { - score += playCounts[i] * getScoreMultiplierForWeek(i); + // bump the count + if (bumpCount) { + playCounts[0]++; } - return score; - } + float score = calculateScore(playCounts); - /** - * Gets the column name for each week # - * - * @param week number - * @return the column name - */ - @NonNull - private static String getColumnNameForWeek(final int week) { - return SongPlayCountColumns.WEEK_PLAY_COUNT + week; - } + // if the score is non-existant, then delete it + if (score < .01f) { + deleteEntry(database, stringId); + } else { + // create the content values + ContentValues values = new ContentValues(NUM_WEEKS + 2); + values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch); + values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score); - /** - * Gets the score multiplier for each week - * - * @param week number - * @return the multiplier to apply - */ - private static float getScoreMultiplierForWeek(final int week) { - return sInterpolator.getInterpolation(1 - (week / (float) NUM_WEEKS)) * INTERPOLATOR_HEIGHT - + INTERPOLATOR_BASE; - } + for (int i = 0; i < NUM_WEEKS; i++) { + values.put(getColumnNameForWeek(i), playCounts[i]); + } - /** - * For some performance gain, return a static value for the column index for a week - * WARNING: This function assumes you have selected all columns for it to work - * - * @param week number - * @return column index of that week - */ - private static int getColumnIndexForWeek(final int week) { - // ID, followed by the weeks columns - return 1 + week; - } - - @Override - public void onCreate(@NonNull final SQLiteDatabase db) { - // create the play count table - // WARNING: If you change the order of these columns - // please update getColumnIndexForWeek - StringBuilder builder = new StringBuilder(); - builder.append("CREATE TABLE IF NOT EXISTS "); - builder.append(SongPlayCountColumns.NAME); - builder.append("("); - builder.append(SongPlayCountColumns.ID); - builder.append(" INT UNIQUE,"); - - for (int i = 0; i < NUM_WEEKS; i++) { - builder.append(getColumnNameForWeek(i)); - builder.append(" INT DEFAULT 0,"); + // update the entry + database.update( + SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS, new String[] {stringId}); } + } else if (bumpCount) { + // else no shifting, just update the scores + ContentValues values = new ContentValues(2); - builder.append(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX); - builder.append(" INT NOT NULL,"); + // increase the score by a single score amount + int scoreIndex = cursor.getColumnIndex(SongPlayCountColumns.PLAY_COUNT_SCORE); + float score = cursor.getFloat(scoreIndex) + getScoreMultiplierForWeek(0); + values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score); - builder.append(SongPlayCountColumns.PLAY_COUNT_SCORE); - builder.append(" REAL DEFAULT 0);"); + // increase the play count by 1 + values.put(getColumnNameForWeek(0), cursor.getInt(getColumnIndexForWeek(0)) + 1); - db.execSQL(builder.toString()); + // update the entry + database.update( + SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS, new String[] {stringId}); + } + + cursor.close(); + } else if (bumpCount) { + // if we have no existing results, create a new one + createNewPlayedEntry(database, id); } - @Override - public void onUpgrade(@NonNull final SQLiteDatabase db, final int oldVersion, final int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME); - onCreate(db); + database.setTransactionSuccessful(); + database.endTransaction(); + } + + public void clear() { + final SQLiteDatabase database = getWritableDatabase(); + database.delete(SongPlayCountColumns.NAME, null, null); + } + + /** + * Gets a cursor containing the top songs played. Note this only returns songs that have been + * played at least once in the past NUM_WEEKS + * + * @param numResults number of results to limit by. If <= 0 it returns all results + * @return the top tracks + */ + public Cursor getTopPlayedResults(int numResults) { + updateResults(); + + final SQLiteDatabase database = getReadableDatabase(); + return database.query( + SongPlayCountColumns.NAME, + new String[] {SongPlayCountColumns.ID}, + null, + null, + null, + null, + SongPlayCountColumns.PLAY_COUNT_SCORE + " DESC", + (numResults <= 0 ? null : String.valueOf(numResults))); + } + + /** + * This updates all the results for the getTopPlayedResults so that we can get an accurate list of + * the top played results + */ + private synchronized void updateResults() { + if (mDatabaseUpdated) { + return; } - @Override - public void onDowngrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - // If we ever have downgrade, drop the table to be safe - db.execSQL("DROP TABLE IF EXISTS " + SongPlayCountColumns.NAME); - onCreate(db); + final SQLiteDatabase database = getWritableDatabase(); + + database.beginTransaction(); + + int oldestWeekWeCareAbout = mNumberOfWeeksSinceEpoch - NUM_WEEKS + 1; + // delete rows we don't care about anymore + database.delete( + SongPlayCountColumns.NAME, + SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX + " < " + oldestWeekWeCareAbout, + null); + + // get the remaining rows + Cursor cursor = + database.query( + SongPlayCountColumns.NAME, + new String[] {SongPlayCountColumns.ID}, + null, + null, + null, + null, + null); + + if (cursor != null && cursor.moveToFirst()) { + // for each row, update it + do { + updateExistingRow(database, cursor.getLong(0), false); + } while (cursor.moveToNext()); + + cursor.close(); } - /** - * Increases the play count of a song by 1 - * - * @param songId The song id to increase the play count - */ - public void bumpPlayCount(final long songId) { - if (songId == -1) { - return; - } + mDatabaseUpdated = true; + database.setTransactionSuccessful(); + database.endTransaction(); + } - final SQLiteDatabase database = getWritableDatabase(); - updateExistingRow(database, songId, true); - } + /** @param songId The song Id to remove. */ + public void removeItem(final long songId) { + final SQLiteDatabase database = getWritableDatabase(); + deleteEntry(database, String.valueOf(songId)); + } - /** - * This creates a new entry that indicates a song has been played once as well as its score - * - * @param database a write able database - * @param songId the id of the track - */ - private void createNewPlayedEntry(@NonNull final SQLiteDatabase database, final long songId) { - // no row exists, create a new one - float newScore = getScoreMultiplierForWeek(0); - int newPlayCount = 1; + /** + * Deletes the entry + * + * @param database database to use + * @param stringId id to delete + */ + private void deleteEntry(@NonNull final SQLiteDatabase database, final String stringId) { + database.delete(SongPlayCountColumns.NAME, WHERE_ID_EQUALS, new String[] {stringId}); + } - final ContentValues values = new ContentValues(3); - values.put(SongPlayCountColumns.ID, songId); - values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, newScore); - values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch); - values.put(getColumnNameForWeek(0), newPlayCount); + public interface SongPlayCountColumns { - database.insert(SongPlayCountColumns.NAME, null, values); - } + String NAME = "song_play_count"; - /** - * This function will take a song entry and update it to the latest week and increase the count - * for the current week by 1 if necessary - * - * @param database a writeable database - * @param id the id of the track to bump - * @param bumpCount whether to bump the current's week play count by 1 and adjust the score - */ - private void updateExistingRow(@NonNull final SQLiteDatabase database, final long id, boolean bumpCount) { - String stringId = String.valueOf(id); + String ID = "song_id"; - // begin the transaction - database.beginTransaction(); + String WEEK_PLAY_COUNT = "week"; - // get the cursor of this content inside the transaction - final Cursor cursor = database.query(SongPlayCountColumns.NAME, null, WHERE_ID_EQUALS, - new String[]{stringId}, null, null, null); + String LAST_UPDATED_WEEK_INDEX = "week_index"; - // if we have a result - if (cursor != null && cursor.moveToFirst()) { - // figure how many weeks since we last updated - int lastUpdatedIndex = cursor.getColumnIndex(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX); - int lastUpdatedWeek = cursor.getInt(lastUpdatedIndex); - int weekDiff = mNumberOfWeeksSinceEpoch - lastUpdatedWeek; - - // if it's more than the number of weeks we track, delete it and create a new entry - if (Math.abs(weekDiff) >= NUM_WEEKS) { - // this entry needs to be dropped since it is too outdated - deleteEntry(database, stringId); - if (bumpCount) { - createNewPlayedEntry(database, id); - } - } else if (weekDiff != 0) { - // else, shift the weeks - int[] playCounts = new int[NUM_WEEKS]; - - if (weekDiff > 0) { - // time is shifted forwards - for (int i = 0; i < NUM_WEEKS - weekDiff; i++) { - playCounts[i + weekDiff] = cursor.getInt(getColumnIndexForWeek(i)); - } - } else if (weekDiff < 0) { - // time is shifted backwards (by user) - nor typical behavior but we - // will still handle it - - // since weekDiff is -ve, NUM_WEEKS + weekDiff is the real # of weeks we have to - // transfer. Then we transfer the old week i - weekDiff to week i - // for example if the user shifted back 2 weeks, ie -2, then for 0 to - // NUM_WEEKS + (-2) we set the new week i = old week i - (-2) or i+2 - for (int i = 0; i < NUM_WEEKS + weekDiff; i++) { - playCounts[i] = cursor.getInt(getColumnIndexForWeek(i - weekDiff)); - } - } - - // bump the count - if (bumpCount) { - playCounts[0]++; - } - - float score = calculateScore(playCounts); - - // if the score is non-existant, then delete it - if (score < .01f) { - deleteEntry(database, stringId); - } else { - // create the content values - ContentValues values = new ContentValues(NUM_WEEKS + 2); - values.put(SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX, mNumberOfWeeksSinceEpoch); - values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score); - - for (int i = 0; i < NUM_WEEKS; i++) { - values.put(getColumnNameForWeek(i), playCounts[i]); - } - - // update the entry - database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS, - new String[]{stringId}); - } - } else if (bumpCount) { - // else no shifting, just update the scores - ContentValues values = new ContentValues(2); - - // increase the score by a single score amount - int scoreIndex = cursor.getColumnIndex(SongPlayCountColumns.PLAY_COUNT_SCORE); - float score = cursor.getFloat(scoreIndex) + getScoreMultiplierForWeek(0); - values.put(SongPlayCountColumns.PLAY_COUNT_SCORE, score); - - // increase the play count by 1 - values.put(getColumnNameForWeek(0), cursor.getInt(getColumnIndexForWeek(0)) + 1); - - // update the entry - database.update(SongPlayCountColumns.NAME, values, WHERE_ID_EQUALS, - new String[]{stringId}); - } - - cursor.close(); - } else if (bumpCount) { - // if we have no existing results, create a new one - createNewPlayedEntry(database, id); - } - - database.setTransactionSuccessful(); - database.endTransaction(); - } - - public void clear() { - final SQLiteDatabase database = getWritableDatabase(); - database.delete(SongPlayCountColumns.NAME, null, null); - } - - /** - * Gets a cursor containing the top songs played. Note this only returns songs that have been - * played at least once in the past NUM_WEEKS - * - * @param numResults number of results to limit by. If <= 0 it returns all results - * @return the top tracks - */ - public Cursor getTopPlayedResults(int numResults) { - updateResults(); - - final SQLiteDatabase database = getReadableDatabase(); - return database.query(SongPlayCountColumns.NAME, new String[]{SongPlayCountColumns.ID}, - null, null, null, null, SongPlayCountColumns.PLAY_COUNT_SCORE + " DESC", - (numResults <= 0 ? null : String.valueOf(numResults))); - } - - /** - * This updates all the results for the getTopPlayedResults so that we can get an - * accurate list of the top played results - */ - private synchronized void updateResults() { - if (mDatabaseUpdated) { - return; - } - - final SQLiteDatabase database = getWritableDatabase(); - - database.beginTransaction(); - - int oldestWeekWeCareAbout = mNumberOfWeeksSinceEpoch - NUM_WEEKS + 1; - // delete rows we don't care about anymore - database.delete(SongPlayCountColumns.NAME, SongPlayCountColumns.LAST_UPDATED_WEEK_INDEX - + " < " + oldestWeekWeCareAbout, null); - - // get the remaining rows - Cursor cursor = database.query(SongPlayCountColumns.NAME, - new String[]{SongPlayCountColumns.ID}, - null, null, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - // for each row, update it - do { - updateExistingRow(database, cursor.getLong(0), false); - } while (cursor.moveToNext()); - - cursor.close(); - } - - mDatabaseUpdated = true; - database.setTransactionSuccessful(); - database.endTransaction(); - } - - /** - * @param songId The song Id to remove. - */ - public void removeItem(final long songId) { - final SQLiteDatabase database = getWritableDatabase(); - deleteEntry(database, String.valueOf(songId)); - } - - /** - * Deletes the entry - * - * @param database database to use - * @param stringId id to delete - */ - private void deleteEntry(@NonNull final SQLiteDatabase database, final String stringId) { - database.delete(SongPlayCountColumns.NAME, WHERE_ID_EQUALS, new String[]{stringId}); - } - - public interface SongPlayCountColumns { - - String NAME = "song_play_count"; - - String ID = "song_id"; - - String WEEK_PLAY_COUNT = "week"; - - String LAST_UPDATED_WEEK_INDEX = "week_index"; - - String PLAY_COUNT_SCORE = "play_count_score"; - } + String PLAY_COUNT_SCORE = "play_count_score"; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SortedCursor.java b/app/src/main/java/code/name/monkey/retromusic/repository/SortedCursor.java index 701d31da0..f7705ca4e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SortedCursor.java +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SortedCursor.java @@ -15,154 +15,150 @@ package code.name.monkey.retromusic.repository; import android.database.AbstractCursor; import android.database.Cursor; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; /** * This cursor basically wraps a song cursor and is given a list of the order of the ids of the - * contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted - * by moving the point to the appropriate spot + * contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted by + * moving the point to the appropriate spot */ public class SortedCursor extends AbstractCursor { - // cursor to wrap - private final Cursor mCursor; - // the map of external indices to internal indices - private ArrayList mOrderedPositions; - // this contains the ids that weren't found in the underlying cursor - private ArrayList mMissingValues; - // this contains the mapped cursor positions and afterwards the extra ids that weren't found - private HashMap mMapCursorPositions; + // cursor to wrap + private final Cursor mCursor; + // the map of external indices to internal indices + private ArrayList mOrderedPositions; + // this contains the ids that weren't found in the underlying cursor + private ArrayList mMissingValues; + // this contains the mapped cursor positions and afterwards the extra ids that weren't found + private HashMap mMapCursorPositions; - /** - * @param cursor to wrap - * @param order the list of unique ids in sorted order to display - * @param columnName the column name of the id to look up in the internal cursor - */ - public SortedCursor(@NonNull final Cursor cursor, @Nullable final String[] order, final String columnName) { - mCursor = cursor; - mMissingValues = buildCursorPositionMapping(order, columnName); - } + /** + * @param cursor to wrap + * @param order the list of unique ids in sorted order to display + * @param columnName the column name of the id to look up in the internal cursor + */ + public SortedCursor( + @NonNull final Cursor cursor, @Nullable final String[] order, final String columnName) { + mCursor = cursor; + mMissingValues = buildCursorPositionMapping(order, columnName); + } - /** - * This function populates mOrderedPositions with the cursor positions in the order based - * on the order passed in - * - * @param order the target order of the internal cursor - * @return returns the ids that aren't found in the underlying cursor - */ - @NonNull - private ArrayList buildCursorPositionMapping(@Nullable final String[] order, final String columnName) { - ArrayList missingValues = new ArrayList<>(); + /** + * This function populates mOrderedPositions with the cursor positions in the order based on the + * order passed in + * + * @param order the target order of the internal cursor + * @return returns the ids that aren't found in the underlying cursor + */ + @NonNull + private ArrayList buildCursorPositionMapping( + @Nullable final String[] order, final String columnName) { + ArrayList missingValues = new ArrayList<>(); - mOrderedPositions = new ArrayList<>(mCursor.getCount()); + mOrderedPositions = new ArrayList<>(mCursor.getCount()); - mMapCursorPositions = new HashMap<>(mCursor.getCount()); - final int valueColumnIndex = mCursor.getColumnIndex(columnName); + mMapCursorPositions = new HashMap<>(mCursor.getCount()); + final int valueColumnIndex = mCursor.getColumnIndex(columnName); - if (mCursor.moveToFirst()) { - // first figure out where each of the ids are in the cursor - do { - mMapCursorPositions.put(mCursor.getString(valueColumnIndex), mCursor.getPosition()); - } while (mCursor.moveToNext()); + if (mCursor.moveToFirst()) { + // first figure out where each of the ids are in the cursor + do { + mMapCursorPositions.put(mCursor.getString(valueColumnIndex), mCursor.getPosition()); + } while (mCursor.moveToNext()); - if (order != null) { - // now create the ordered positions to map to the internal cursor given the - // external sort order - for (final String value : order) { - if (mMapCursorPositions.containsKey(value)) { - mOrderedPositions.add(mMapCursorPositions.get(value)); - mMapCursorPositions.remove(value); - } else { - missingValues.add(value); - } - } - } - - mCursor.moveToFirst(); + if (order != null) { + // now create the ordered positions to map to the internal cursor given the + // external sort order + for (final String value : order) { + if (mMapCursorPositions.containsKey(value)) { + mOrderedPositions.add(mMapCursorPositions.get(value)); + mMapCursorPositions.remove(value); + } else { + missingValues.add(value); + } } + } - return missingValues; + mCursor.moveToFirst(); } - /** - * @return the list of ids that weren't found in the underlying cursor - */ - public ArrayList getMissingValues() { - return mMissingValues; + return missingValues; + } + + /** @return the list of ids that weren't found in the underlying cursor */ + public ArrayList getMissingValues() { + return mMissingValues; + } + + /** @return the list of ids that were in the underlying cursor but not part of the ordered list */ + @NonNull + public Collection getExtraValues() { + return mMapCursorPositions.keySet(); + } + + @Override + public void close() { + mCursor.close(); + + super.close(); + } + + @Override + public int getCount() { + return mOrderedPositions.size(); + } + + @Override + public String[] getColumnNames() { + return mCursor.getColumnNames(); + } + + @Override + public String getString(int column) { + return mCursor.getString(column); + } + + @Override + public short getShort(int column) { + return mCursor.getShort(column); + } + + @Override + public int getInt(int column) { + return mCursor.getInt(column); + } + + @Override + public long getLong(int column) { + return mCursor.getLong(column); + } + + @Override + public float getFloat(int column) { + return mCursor.getFloat(column); + } + + @Override + public double getDouble(int column) { + return mCursor.getDouble(column); + } + + @Override + public boolean isNull(int column) { + return mCursor.isNull(column); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + if (newPosition >= 0 && newPosition < getCount()) { + mCursor.moveToPosition(mOrderedPositions.get(newPosition)); + return true; } - /** - * @return the list of ids that were in the underlying cursor but not part of the ordered list - */ - @NonNull - public Collection getExtraValues() { - return mMapCursorPositions.keySet(); - } - - @Override - public void close() { - mCursor.close(); - - super.close(); - } - - @Override - public int getCount() { - return mOrderedPositions.size(); - } - - @Override - public String[] getColumnNames() { - return mCursor.getColumnNames(); - } - - @Override - public String getString(int column) { - return mCursor.getString(column); - } - - @Override - public short getShort(int column) { - return mCursor.getShort(column); - } - - @Override - public int getInt(int column) { - return mCursor.getInt(column); - } - - @Override - public long getLong(int column) { - return mCursor.getLong(column); - } - - @Override - public float getFloat(int column) { - return mCursor.getFloat(column); - } - - @Override - public double getDouble(int column) { - return mCursor.getDouble(column); - } - - @Override - public boolean isNull(int column) { - return mCursor.isNull(column); - } - - @Override - public boolean onMove(int oldPosition, int newPosition) { - if (newPosition >= 0 && newPosition < getCount()) { - mCursor.moveToPosition(mOrderedPositions.get(newPosition)); - return true; - } - - return false; - } + return false; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/SortedLongCursor.java b/app/src/main/java/code/name/monkey/retromusic/repository/SortedLongCursor.java index 727cddd88..02ade994b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/SortedLongCursor.java +++ b/app/src/main/java/code/name/monkey/retromusic/repository/SortedLongCursor.java @@ -15,154 +15,149 @@ package code.name.monkey.retromusic.repository; import android.database.AbstractCursor; import android.database.Cursor; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; /** * This cursor basically wraps a song cursor and is given a list of the order of the ids of the - * contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted - * by moving the point to the appropriate spot + * contents of the cursor. It wraps the Cursor and simulates the internal cursor being sorted by + * moving the point to the appropriate spot */ public class SortedLongCursor extends AbstractCursor { - // cursor to wrap - private final Cursor mCursor; - // the map of external indices to internal indices - private ArrayList mOrderedPositions; - // this contains the ids that weren't found in the underlying cursor - private ArrayList mMissingIds; - // this contains the mapped cursor positions and afterwards the extra ids that weren't found - private HashMap mMapCursorPositions; + // cursor to wrap + private final Cursor mCursor; + // the map of external indices to internal indices + private ArrayList mOrderedPositions; + // this contains the ids that weren't found in the underlying cursor + private ArrayList mMissingIds; + // this contains the mapped cursor positions and afterwards the extra ids that weren't found + private HashMap mMapCursorPositions; - /** - * @param cursor to wrap - * @param order the list of unique ids in sorted order to display - * @param columnName the column name of the id to look up in the internal cursor - */ - public SortedLongCursor(final Cursor cursor, final long[] order, final String columnName) { + /** + * @param cursor to wrap + * @param order the list of unique ids in sorted order to display + * @param columnName the column name of the id to look up in the internal cursor + */ + public SortedLongCursor(final Cursor cursor, final long[] order, final String columnName) { - mCursor = cursor; - mMissingIds = buildCursorPositionMapping(order, columnName); - } + mCursor = cursor; + mMissingIds = buildCursorPositionMapping(order, columnName); + } - /** - * This function populates mOrderedPositions with the cursor positions in the order based - * on the order passed in - * - * @param order the target order of the internal cursor - * @return returns the ids that aren't found in the underlying cursor - */ - @NonNull - private ArrayList buildCursorPositionMapping(@Nullable final long[] order, final String columnName) { - ArrayList missingIds = new ArrayList<>(); + /** + * This function populates mOrderedPositions with the cursor positions in the order based on the + * order passed in + * + * @param order the target order of the internal cursor + * @return returns the ids that aren't found in the underlying cursor + */ + @NonNull + private ArrayList buildCursorPositionMapping( + @Nullable final long[] order, final String columnName) { + ArrayList missingIds = new ArrayList<>(); - mOrderedPositions = new ArrayList<>(mCursor.getCount()); + mOrderedPositions = new ArrayList<>(mCursor.getCount()); - mMapCursorPositions = new HashMap<>(mCursor.getCount()); - final int idPosition = mCursor.getColumnIndex(columnName); + mMapCursorPositions = new HashMap<>(mCursor.getCount()); + final int idPosition = mCursor.getColumnIndex(columnName); - if (mCursor.moveToFirst()) { - // first figure out where each of the ids are in the cursor - do { - mMapCursorPositions.put(mCursor.getLong(idPosition), mCursor.getPosition()); - } while (mCursor.moveToNext()); + if (mCursor.moveToFirst()) { + // first figure out where each of the ids are in the cursor + do { + mMapCursorPositions.put(mCursor.getLong(idPosition), mCursor.getPosition()); + } while (mCursor.moveToNext()); - // now create the ordered positions to map to the internal cursor given the - // external sort order - for (int i = 0; order != null && i < order.length; i++) { - final long id = order[i]; - if (mMapCursorPositions.containsKey(id)) { - mOrderedPositions.add(mMapCursorPositions.get(id)); - mMapCursorPositions.remove(id); - } else { - missingIds.add(id); - } - } - - mCursor.moveToFirst(); + // now create the ordered positions to map to the internal cursor given the + // external sort order + for (int i = 0; order != null && i < order.length; i++) { + final long id = order[i]; + if (mMapCursorPositions.containsKey(id)) { + mOrderedPositions.add(mMapCursorPositions.get(id)); + mMapCursorPositions.remove(id); + } else { + missingIds.add(id); } + } - return missingIds; + mCursor.moveToFirst(); } - /** - * @return the list of ids that weren't found in the underlying cursor - */ - public ArrayList getMissingIds() { - return mMissingIds; + return missingIds; + } + + /** @return the list of ids that weren't found in the underlying cursor */ + public ArrayList getMissingIds() { + return mMissingIds; + } + + /** @return the list of ids that were in the underlying cursor but not part of the ordered list */ + @NonNull + public Collection getExtraIds() { + return mMapCursorPositions.keySet(); + } + + @Override + public void close() { + mCursor.close(); + + super.close(); + } + + @Override + public int getCount() { + return mOrderedPositions.size(); + } + + @Override + public String[] getColumnNames() { + return mCursor.getColumnNames(); + } + + @Override + public String getString(int column) { + return mCursor.getString(column); + } + + @Override + public short getShort(int column) { + return mCursor.getShort(column); + } + + @Override + public int getInt(int column) { + return mCursor.getInt(column); + } + + @Override + public long getLong(int column) { + return mCursor.getLong(column); + } + + @Override + public float getFloat(int column) { + return mCursor.getFloat(column); + } + + @Override + public double getDouble(int column) { + return mCursor.getDouble(column); + } + + @Override + public boolean isNull(int column) { + return mCursor.isNull(column); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + if (newPosition >= 0 && newPosition < getCount()) { + mCursor.moveToPosition(mOrderedPositions.get(newPosition)); + return true; } - /** - * @return the list of ids that were in the underlying cursor but not part of the ordered list - */ - @NonNull - public Collection getExtraIds() { - return mMapCursorPositions.keySet(); - } - - @Override - public void close() { - mCursor.close(); - - super.close(); - } - - @Override - public int getCount() { - return mOrderedPositions.size(); - } - - @Override - public String[] getColumnNames() { - return mCursor.getColumnNames(); - } - - @Override - public String getString(int column) { - return mCursor.getString(column); - } - - @Override - public short getShort(int column) { - return mCursor.getShort(column); - } - - @Override - public int getInt(int column) { - return mCursor.getInt(column); - } - - @Override - public long getLong(int column) { - return mCursor.getLong(column); - } - - @Override - public float getFloat(int column) { - return mCursor.getFloat(column); - } - - @Override - public double getDouble(int column) { - return mCursor.getDouble(column); - } - - @Override - public boolean isNull(int column) { - return mCursor.isNull(column); - } - - @Override - public boolean onMove(int oldPosition, int newPosition) { - if (newPosition >= 0 && newPosition < getCount()) { - mCursor.moveToPosition(mOrderedPositions.get(newPosition)); - return true; - } - - return false; - } + return false; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java index 5f49414c1..9ed12f5ea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.java @@ -23,327 +23,300 @@ import android.net.Uri; import android.os.PowerManager; import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.PreferenceUtil; -/** - * @author Andrew Neal, Karim Abou Zeid (kabouzeid) - */ -public class MultiPlayer implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { - public static final String TAG = MultiPlayer.class.getSimpleName(); +/** @author Andrew Neal, Karim Abou Zeid (kabouzeid) */ +public class MultiPlayer + implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { + public static final String TAG = MultiPlayer.class.getSimpleName(); - private MediaPlayer mCurrentMediaPlayer = new MediaPlayer(); - private MediaPlayer mNextMediaPlayer; + private MediaPlayer mCurrentMediaPlayer = new MediaPlayer(); + private MediaPlayer mNextMediaPlayer; - private Context context; - @Nullable - private Playback.PlaybackCallbacks callbacks; + private Context context; + @Nullable private Playback.PlaybackCallbacks callbacks; - private boolean mIsInitialized = false; + private boolean mIsInitialized = false; - /** - * Constructor of MultiPlayer - */ - MultiPlayer(final Context context) { - this.context = context; - mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + /** Constructor of MultiPlayer */ + MultiPlayer(final Context context) { + this.context = context; + mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + } + + /** + * @param path The path of the file, or the http/rtsp URL of the stream you want to play + * @return True if the player has been prepared and is ready to play, false otherwise + */ + @Override + public boolean setDataSource(@NonNull final String path) { + mIsInitialized = false; + mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path); + if (mIsInitialized) { + setNextDataSource(null); } + return mIsInitialized; + } - /** - * @param path The path of the file, or the http/rtsp URL of the stream - * you want to play - * @return True if the player has been prepared and is - * ready to play, false otherwise - */ - @Override - public boolean setDataSource(@NonNull final String path) { - mIsInitialized = false; - mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path); - if (mIsInitialized) { - setNextDataSource(null); - } - return mIsInitialized; + /** + * @param player The {@link MediaPlayer} to use + * @param path The path of the file, or the http/rtsp URL of the stream you want to play + * @return True if the player has been prepared and is ready to play, false otherwise + */ + private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) { + if (context == null) { + return false; } + try { + player.reset(); + player.setOnPreparedListener(null); + if (path.startsWith("content://")) { + player.setDataSource(context, Uri.parse(path)); + } else { + player.setDataSource(path); + } + player.setAudioStreamType(AudioManager.STREAM_MUSIC); + player.prepare(); + } catch (Exception e) { + return false; + } + player.setOnCompletionListener(this); + player.setOnErrorListener(this); + final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); + intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); + intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); + context.sendBroadcast(intent); + return true; + } - /** - * @param player The {@link MediaPlayer} to use - * @param path The path of the file, or the http/rtsp URL of the stream - * you want to play - * @return True if the player has been prepared and is - * ready to play, false otherwise - */ - private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) { - if (context == null) { - return false; - } + /** + * Set the MediaPlayer to start when this MediaPlayer finishes playback. + * + * @param path The path of the file, or the http/rtsp URL of the stream you want to play + */ + @Override + public void setNextDataSource(@Nullable final String path) { + if (context == null) { + return; + } + try { + mCurrentMediaPlayer.setNextMediaPlayer(null); + } catch (IllegalArgumentException e) { + Log.i(TAG, "Next media player is current one, continuing"); + } catch (IllegalStateException e) { + Log.e(TAG, "Media player not initialized!"); + return; + } + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); + mNextMediaPlayer = null; + } + if (path == null) { + return; + } + if (PreferenceUtil.INSTANCE.isGapLessPlayback()) { + mNextMediaPlayer = new MediaPlayer(); + mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + mNextMediaPlayer.setAudioSessionId(getAudioSessionId()); + if (setDataSourceImpl(mNextMediaPlayer, path)) { try { - player.reset(); - player.setOnPreparedListener(null); - if (path.startsWith("content://")) { - player.setDataSource(context, Uri.parse(path)); - } else { - player.setDataSource(path); - } - player.setAudioStreamType(AudioManager.STREAM_MUSIC); - player.prepare(); - } catch (Exception e) { - return false; - } - player.setOnCompletionListener(this); - player.setOnErrorListener(this); - final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); - intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); - intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); - context.sendBroadcast(intent); - return true; - } - - /** - * Set the MediaPlayer to start when this MediaPlayer finishes playback. - * - * @param path The path of the file, or the http/rtsp URL of the stream - * you want to play - */ - @Override - public void setNextDataSource(@Nullable final String path) { - if (context == null) { - return; - } - try { - mCurrentMediaPlayer.setNextMediaPlayer(null); - } catch (IllegalArgumentException e) { - Log.i(TAG, "Next media player is current one, continuing"); - } catch (IllegalStateException e) { - Log.e(TAG, "Media player not initialized!"); - return; - } - if (mNextMediaPlayer != null) { - mNextMediaPlayer.release(); - mNextMediaPlayer = null; - } - if (path == null) { - return; - } - if (PreferenceUtil.INSTANCE.isGapLessPlayback()) { - mNextMediaPlayer = new MediaPlayer(); - mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - mNextMediaPlayer.setAudioSessionId(getAudioSessionId()); - if (setDataSourceImpl(mNextMediaPlayer, path)) { - try { - mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer); - } catch (@NonNull IllegalArgumentException | IllegalStateException e) { - Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e); - if (mNextMediaPlayer != null) { - mNextMediaPlayer.release(); - mNextMediaPlayer = null; - } - } - } else { - if (mNextMediaPlayer != null) { - mNextMediaPlayer.release(); - mNextMediaPlayer = null; - } - } - } - } - - /** - * Sets the callbacks - * - * @param callbacks The callbacks to use - */ - @Override - public void setCallbacks(@Nullable final Playback.PlaybackCallbacks callbacks) { - this.callbacks = callbacks; - } - - /** - * @return True if the player is ready to go, false otherwise - */ - @Override - public boolean isInitialized() { - return mIsInitialized; - } - - /** - * Starts or resumes playback. - */ - @Override - public boolean start() { - try { - mCurrentMediaPlayer.start(); - return true; - } catch (IllegalStateException e) { - return false; - } - } - - /** - * Resets the MediaPlayer to its uninitialized state. - */ - @Override - public void stop() { - mCurrentMediaPlayer.reset(); - mIsInitialized = false; - } - - /** - * Releases resources associated with this MediaPlayer object. - */ - @Override - public void release() { - stop(); - mCurrentMediaPlayer.release(); - if (mNextMediaPlayer != null) { - mNextMediaPlayer.release(); - } - } - - /** - * Pauses playback. Call start() to resume. - */ - @Override - public boolean pause() { - try { - mCurrentMediaPlayer.pause(); - return true; - } catch (IllegalStateException e) { - return false; - } - } - - /** - * Checks whether the MultiPlayer is playing. - */ - @Override - public boolean isPlaying() { - return mIsInitialized && mCurrentMediaPlayer.isPlaying(); - } - - /** - * Gets the duration of the file. - * - * @return The duration in milliseconds - */ - @Override - public int duration() { - if (!mIsInitialized) { - return -1; - } - try { - return mCurrentMediaPlayer.getDuration(); - } catch (IllegalStateException e) { - return -1; - } - } - - /** - * Gets the current playback position. - * - * @return The current position in milliseconds - */ - @Override - public int position() { - if (!mIsInitialized) { - return -1; - } - try { - return mCurrentMediaPlayer.getCurrentPosition(); - } catch (IllegalStateException e) { - return -1; - } - } - - /** - * Gets the current playback position. - * - * @param whereto The offset in milliseconds from the start to seek to - * @return The offset in milliseconds from the start to seek to - */ - @Override - public int seek(final int whereto) { - try { - mCurrentMediaPlayer.seekTo(whereto); - return whereto; - } catch (IllegalStateException e) { - return -1; - } - } - - @Override - public boolean setVolume(final float vol) { - try { - mCurrentMediaPlayer.setVolume(vol, vol); - return true; - } catch (IllegalStateException e) { - return false; - } - } - - /** - * Sets the audio session ID. - * - * @param sessionId The audio session ID - */ - @Override - public boolean setAudioSessionId(final int sessionId) { - try { - mCurrentMediaPlayer.setAudioSessionId(sessionId); - return true; + mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer); } catch (@NonNull IllegalArgumentException | IllegalStateException e) { - return false; - } - } - - /** - * Returns the audio session ID. - * - * @return The current audio session ID. - */ - @Override - public int getAudioSessionId() { - return mCurrentMediaPlayer.getAudioSessionId(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onError(final MediaPlayer mp, final int what, final int extra) { - mIsInitialized = false; - mCurrentMediaPlayer.release(); - mCurrentMediaPlayer = new MediaPlayer(); - mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - if (context != null) { - Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); - } - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void onCompletion(final MediaPlayer mp) { - if (mp.equals(mCurrentMediaPlayer) && mNextMediaPlayer != null) { - mIsInitialized = false; - mCurrentMediaPlayer.release(); - mCurrentMediaPlayer = mNextMediaPlayer; - mIsInitialized = true; + Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e); + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); mNextMediaPlayer = null; - if (callbacks != null) - callbacks.onTrackWentToNext(); - } else { - if (callbacks != null) - callbacks.onTrackEnded(); + } } + } else { + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); + mNextMediaPlayer = null; + } + } } + } + /** + * Sets the callbacks + * + * @param callbacks The callbacks to use + */ + @Override + public void setCallbacks(@Nullable final Playback.PlaybackCallbacks callbacks) { + this.callbacks = callbacks; + } -} \ No newline at end of file + /** @return True if the player is ready to go, false otherwise */ + @Override + public boolean isInitialized() { + return mIsInitialized; + } + + /** Starts or resumes playback. */ + @Override + public boolean start() { + try { + mCurrentMediaPlayer.start(); + return true; + } catch (IllegalStateException e) { + return false; + } + } + + /** Resets the MediaPlayer to its uninitialized state. */ + @Override + public void stop() { + mCurrentMediaPlayer.reset(); + mIsInitialized = false; + } + + /** Releases resources associated with this MediaPlayer object. */ + @Override + public void release() { + stop(); + mCurrentMediaPlayer.release(); + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); + } + } + + /** Pauses playback. Call start() to resume. */ + @Override + public boolean pause() { + try { + mCurrentMediaPlayer.pause(); + return true; + } catch (IllegalStateException e) { + return false; + } + } + + /** Checks whether the MultiPlayer is playing. */ + @Override + public boolean isPlaying() { + return mIsInitialized && mCurrentMediaPlayer.isPlaying(); + } + + /** + * Gets the duration of the file. + * + * @return The duration in milliseconds + */ + @Override + public int duration() { + if (!mIsInitialized) { + return -1; + } + try { + return mCurrentMediaPlayer.getDuration(); + } catch (IllegalStateException e) { + return -1; + } + } + + /** + * Gets the current playback position. + * + * @return The current position in milliseconds + */ + @Override + public int position() { + if (!mIsInitialized) { + return -1; + } + try { + return mCurrentMediaPlayer.getCurrentPosition(); + } catch (IllegalStateException e) { + return -1; + } + } + + /** + * Gets the current playback position. + * + * @param whereto The offset in milliseconds from the start to seek to + * @return The offset in milliseconds from the start to seek to + */ + @Override + public int seek(final int whereto) { + try { + mCurrentMediaPlayer.seekTo(whereto); + return whereto; + } catch (IllegalStateException e) { + return -1; + } + } + + @Override + public boolean setVolume(final float vol) { + try { + mCurrentMediaPlayer.setVolume(vol, vol); + return true; + } catch (IllegalStateException e) { + return false; + } + } + + /** + * Sets the audio session ID. + * + * @param sessionId The audio session ID + */ + @Override + public boolean setAudioSessionId(final int sessionId) { + try { + mCurrentMediaPlayer.setAudioSessionId(sessionId); + return true; + } catch (@NonNull IllegalArgumentException | IllegalStateException e) { + return false; + } + } + + /** + * Returns the audio session ID. + * + * @return The current audio session ID. + */ + @Override + public int getAudioSessionId() { + return mCurrentMediaPlayer.getAudioSessionId(); + } + + /** {@inheritDoc} */ + @Override + public boolean onError(final MediaPlayer mp, final int what, final int extra) { + mIsInitialized = false; + mCurrentMediaPlayer.release(); + mCurrentMediaPlayer = new MediaPlayer(); + mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + if (context != null) { + Toast.makeText( + context, + context.getResources().getString(R.string.unplayable_file), + Toast.LENGTH_SHORT) + .show(); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public void onCompletion(final MediaPlayer mp) { + if (mp.equals(mCurrentMediaPlayer) && mNextMediaPlayer != null) { + mIsInitialized = false; + mCurrentMediaPlayer.release(); + mCurrentMediaPlayer = mNextMediaPlayer; + mIsInitialized = true; + mNextMediaPlayer = null; + if (callbacks != null) callbacks.onTrackWentToNext(); + } else { + if (callbacks != null) callbacks.onTrackEnded(); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index ebc0a9add..f9abe7d60 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -14,6 +14,13 @@ package code.name.monkey.retromusic.service; +import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN; +import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART; +import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION; +import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION; +import static code.name.monkey.retromusic.ConstantsKt.GAP_LESS_PLAYBACK; +import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; + import android.app.PendingIntent; import android.app.Service; import android.appwidget.AppWidgetManager; @@ -47,21 +54,9 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; - -import com.bumptech.glide.BitmapRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.SimpleTarget; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Random; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.LockScreenActivity; import code.name.monkey.retromusic.appwidgets.AppWidgetBig; @@ -84,1304 +79,1368 @@ import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.MusicUtil; import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroUtil; +import com.bumptech.glide.BitmapRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.SimpleTarget; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; -import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN; -import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART; -import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.GAP_LESS_PLAYBACK; -import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; +/** @author Karim Abou Zeid (kabouzeid), Andrew Neal */ +public class MusicService extends Service + implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { -/** - * @author Karim Abou Zeid (kabouzeid), Andrew Neal - */ -public class MusicService extends Service implements - SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { + public static final String TAG = MusicService.class.getSimpleName(); + public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; + public static final String MUSIC_PACKAGE_NAME = "com.android.music"; + public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; + public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; + public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; + public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; + public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; + public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; + public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; + public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; + public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; + public static final String INTENT_EXTRA_PLAYLIST = + RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; + public static final String INTENT_EXTRA_SHUFFLE_MODE = + RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; + public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; + public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; + // Do not change these three strings as it will break support with other apps (e.g. last.fm + // scrobbling) + public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; + public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; + public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; + public static final String FAVORITE_STATE_CHANGED = + RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; + public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; + public static final String SHUFFLE_MODE_CHANGED = + RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; + public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; + public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; + public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; + public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; + public static final String SAVED_POSITION = "POSITION"; + public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; + public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; + public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; + public static final int RELEASE_WAKELOCK = 0; + public static final int TRACK_ENDED = 1; + public static final int TRACK_WENT_TO_NEXT = 2; + public static final int PLAY_SONG = 3; + public static final int PREPARE_NEXT = 4; + public static final int SET_POSITION = 5; + public static final int FOCUS_CHANGE = 6; + public static final int DUCK = 7; + public static final int UNDUCK = 8; + public static final int RESTORE_QUEUES = 9; + public static final int SHUFFLE_MODE_NONE = 0; + public static final int SHUFFLE_MODE_SHUFFLE = 1; + public static final int REPEAT_MODE_NONE = 0; + public static final int REPEAT_MODE_ALL = 1; + public static final int REPEAT_MODE_THIS = 2; + public static final int SAVE_QUEUES = 0; + private static final long MEDIA_SESSION_ACTIONS = + PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_SEEK_TO; + private final IBinder musicBind = new MusicBinder(); + public int nextPosition = -1; - public static final String TAG = MusicService.class.getSimpleName(); - public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; - public static final String MUSIC_PACKAGE_NAME = "com.android.music"; - public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; - public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; - public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; - public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; - public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; - public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; - public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; - public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; - public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; - public static final String INTENT_EXTRA_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; - public static final String INTENT_EXTRA_SHUFFLE_MODE = RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; - public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; - public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; - // Do not change these three strings as it will break support with other apps (e.g. last.fm scrobbling) - public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; - public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; - public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; - public static final String FAVORITE_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; - public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; - public static final String SHUFFLE_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; - public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; - public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; - public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; - public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; - public static final String SAVED_POSITION = "POSITION"; - public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; - public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; - public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; - public static final int RELEASE_WAKELOCK = 0; - public static final int TRACK_ENDED = 1; - public static final int TRACK_WENT_TO_NEXT = 2; - public static final int PLAY_SONG = 3; - public static final int PREPARE_NEXT = 4; - public static final int SET_POSITION = 5; - public static final int FOCUS_CHANGE = 6; - public static final int DUCK = 7; - public static final int UNDUCK = 8; - public static final int RESTORE_QUEUES = 9; - public static final int SHUFFLE_MODE_NONE = 0; - public static final int SHUFFLE_MODE_SHUFFLE = 1; - public static final int REPEAT_MODE_NONE = 0; - public static final int REPEAT_MODE_ALL = 1; - public static final int REPEAT_MODE_THIS = 2; - public static final int SAVE_QUEUES = 0; - private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_STOP - | PlaybackStateCompat.ACTION_SEEK_TO; - private final IBinder musicBind = new MusicBinder(); - public int nextPosition = -1; + public boolean pendingQuit = false; - public boolean pendingQuit = false; + @Nullable public Playback playback; - @Nullable - public Playback playback; + public int position = -1; - public int position = -1; + private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); - private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); + private AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance(); - private AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance(); + private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); - private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); + private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); - private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); + private AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance(); - private AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance(); - - private final BroadcastReceiver widgetIntentReceiver = new BroadcastReceiver() { + private final BroadcastReceiver widgetIntentReceiver = + new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - final String command = intent.getStringExtra(EXTRA_APP_WIDGET_NAME); - final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); - if (command != null) { - switch (command) { - case AppWidgetClassic.NAME: { - appWidgetClassic.performUpdate(MusicService.this, ids); - break; - } - case AppWidgetSmall.NAME: { - appWidgetSmall.performUpdate(MusicService.this, ids); - break; - } - case AppWidgetBig.NAME: { - appWidgetBig.performUpdate(MusicService.this, ids); - break; - } - case AppWidgetCard.NAME: { - appWidgetCard.performUpdate(MusicService.this, ids); - break; - } - case AppWidgetText.NAME: { - appWidgetText.performUpdate(MusicService.this, ids); - break; - } + final String command = intent.getStringExtra(EXTRA_APP_WIDGET_NAME); + final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (command != null) { + switch (command) { + case AppWidgetClassic.NAME: + { + appWidgetClassic.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetSmall.NAME: + { + appWidgetSmall.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetBig.NAME: + { + appWidgetBig.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetCard.NAME: + { + appWidgetCard.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetText.NAME: + { + appWidgetText.performUpdate(MusicService.this, ids); + break; } } - + } } - }; - private AudioManager audioManager; - private IntentFilter becomingNoisyReceiverIntentFilter = new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY); - private boolean becomingNoisyReceiverRegistered; - private IntentFilter bluetoothConnectedIntentFilter = new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); - private boolean bluetoothConnectedRegistered = false; - private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - private boolean headsetReceiverRegistered = false; - private MediaSessionCompat mediaSession; - private ContentObserver mediaStoreObserver; - private HandlerThread musicPlayerHandlerThread; - private boolean notHandledMetaChangedForCurrentTrack; - private List originalPlayingQueue = new ArrayList<>(); - private List playingQueue = new ArrayList<>(); - private boolean pausedByTransientLossOfFocus; + }; + private AudioManager audioManager; + private IntentFilter becomingNoisyReceiverIntentFilter = + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + private boolean becomingNoisyReceiverRegistered; + private IntentFilter bluetoothConnectedIntentFilter = + new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); + private boolean bluetoothConnectedRegistered = false; + private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + private boolean headsetReceiverRegistered = false; + private MediaSessionCompat mediaSession; + private ContentObserver mediaStoreObserver; + private HandlerThread musicPlayerHandlerThread; + private boolean notHandledMetaChangedForCurrentTrack; + private List originalPlayingQueue = new ArrayList<>(); + private List playingQueue = new ArrayList<>(); + private boolean pausedByTransientLossOfFocus; - private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() { + private final BroadcastReceiver becomingNoisyReceiver = + new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { - if (intent.getAction() != null && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { - pause(); - } + if (intent.getAction() != null + && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { + pause(); + } } - }; + }; - private PlaybackHandler playerHandler; + private PlaybackHandler playerHandler; - private final AudioManager.OnAudioFocusChangeListener audioFocusListener - = new AudioManager.OnAudioFocusChangeListener() { + private final AudioManager.OnAudioFocusChangeListener audioFocusListener = + new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(final int focusChange) { - playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); + playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); } - }; + }; - private PlayingNotification playingNotification; - private final BroadcastReceiver updateFavoriteReceiver = new BroadcastReceiver() { + private PlayingNotification playingNotification; + private final BroadcastReceiver updateFavoriteReceiver = + new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - updateNotification(); + updateNotification(); } - }; - private final BroadcastReceiver lockScreenReceiver = new BroadcastReceiver() { + }; + private final BroadcastReceiver lockScreenReceiver = + new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) { - Intent lockIntent = new Intent(context, LockScreenActivity.class); - lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(lockIntent); - } + if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) { + Intent lockIntent = new Intent(context, LockScreenActivity.class); + lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(lockIntent); + } } - }; - private QueueSaveHandler queueSaveHandler; - private HandlerThread queueSaveHandlerThread; - private boolean queuesRestored; - private int repeatMode; - private int shuffleMode; - private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); - private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { + }; + private QueueSaveHandler queueSaveHandler; + private HandlerThread queueSaveHandlerThread; + private boolean queuesRestored; + private int repeatMode; + private int shuffleMode; + private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); + private final BroadcastReceiver bluetoothReceiver = + new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action) && - PreferenceUtil.INSTANCE.isBluetoothSpeaker()) { - if (VERSION.SDK_INT >= VERSION_CODES.M) { - if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) { - play(); - } - } else { - if (getAudioManager().isBluetoothA2dpOn()) { - play(); - } - } + String action = intent.getAction(); + if (action != null) { + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action) + && PreferenceUtil.INSTANCE.isBluetoothSpeaker()) { + if (VERSION.SDK_INT >= VERSION_CODES.M) { + if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) { + play(); } + } else { + if (getAudioManager().isBluetoothA2dpOn()) { + play(); + } + } } + } } - }; - private PhoneStateListener phoneStateListener = new PhoneStateListener() { + }; + private PhoneStateListener phoneStateListener = + new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { - switch (state) { - case TelephonyManager.CALL_STATE_IDLE: - //Not in call: Play music - play(); - break; - case TelephonyManager.CALL_STATE_RINGING: - case TelephonyManager.CALL_STATE_OFFHOOK: - //A call is dialing, active or on hold - pause(); - break; - default: - } - super.onCallStateChanged(state, incomingNumber); + switch (state) { + case TelephonyManager.CALL_STATE_IDLE: + // Not in call: Play music + play(); + break; + case TelephonyManager.CALL_STATE_RINGING: + case TelephonyManager.CALL_STATE_OFFHOOK: + // A call is dialing, active or on hold + pause(); + break; + default: + } + super.onCallStateChanged(state, incomingNumber); } - }; - private BroadcastReceiver headsetReceiver = new BroadcastReceiver() { + }; + private BroadcastReceiver headsetReceiver = + new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (Intent.ACTION_HEADSET_PLUG.equals(action)) { - int state = intent.getIntExtra("state", -1); - switch (state) { - case 0: - pause(); - break; - case 1: - play(); - break; - } - } + String action = intent.getAction(); + if (action != null) { + if (Intent.ACTION_HEADSET_PLUG.equals(action)) { + int state = intent.getIntExtra("state", -1); + switch (state) { + case 0: + pause(); + break; + case 1: + play(); + break; + } } + } } - }; - private ThrottledSeekHandler throttledSeekHandler; - private Handler uiThreadHandler; - private PowerManager.WakeLock wakeLock; + }; + private ThrottledSeekHandler throttledSeekHandler; + private Handler uiThreadHandler; + private PowerManager.WakeLock wakeLock; - private static Bitmap copy(Bitmap bitmap) { - Bitmap.Config config = bitmap.getConfig(); - if (config == null) { - config = Bitmap.Config.RGB_565; + private static Bitmap copy(Bitmap bitmap) { + Bitmap.Config config = bitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.RGB_565; + } + try { + return bitmap.copy(config, false); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + return null; + } + } + + private static String getTrackUri(@NonNull Song song) { + return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); + } + + @Override + public void onCreate() { + super.onCreate(); + final TelephonyManager telephonyManager = + (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + if (telephonyManager != null) { + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + } + + final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); + } + wakeLock.setReferenceCounted(false); + + musicPlayerHandlerThread = new HandlerThread("PlaybackHandler"); + musicPlayerHandlerThread.start(); + playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); + + playback = new MultiPlayer(this); + playback.setCallbacks(this); + + setupMediaSession(); + + // queue saving needs to run on a separate thread so that it doesn't block the playback handler + // events + queueSaveHandlerThread = + new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND); + queueSaveHandlerThread.start(); + queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper()); + + uiThreadHandler = new Handler(); + + registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE)); + registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); + registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); + + initNotification(); + + mediaStoreObserver = new MediaStoreObserver(this, playerHandler); + throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + + PreferenceUtil.INSTANCE.registerOnSharedPreferenceChangedListener(this); + + restoreState(); + + sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED")); + + registerHeadsetEvents(); + registerBluetoothConnected(); + } + + @Override + public void onDestroy() { + unregisterReceiver(widgetIntentReceiver); + unregisterReceiver(updateFavoriteReceiver); + unregisterReceiver(lockScreenReceiver); + if (becomingNoisyReceiverRegistered) { + unregisterReceiver(becomingNoisyReceiver); + becomingNoisyReceiverRegistered = false; + } + if (headsetReceiverRegistered) { + unregisterReceiver(headsetReceiver); + headsetReceiverRegistered = false; + } + if (bluetoothConnectedRegistered) { + unregisterReceiver(bluetoothReceiver); + bluetoothConnectedRegistered = false; + } + mediaSession.setActive(false); + quit(); + releaseResources(); + getContentResolver().unregisterContentObserver(mediaStoreObserver); + PreferenceUtil.INSTANCE.unregisterOnSharedPreferenceChangedListener(this); + wakeLock.release(); + + sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); + } + + public void acquireWakeLock(long milli) { + wakeLock.acquire(milli); + } + + public void addSong(int position, Song song) { + playingQueue.add(position, song); + originalPlayingQueue.add(position, song); + notifyChange(QUEUE_CHANGED); + } + + public void addSong(Song song) { + playingQueue.add(song); + originalPlayingQueue.add(song); + notifyChange(QUEUE_CHANGED); + } + + public void addSongs(int position, List songs) { + playingQueue.addAll(position, songs); + originalPlayingQueue.addAll(position, songs); + notifyChange(QUEUE_CHANGED); + } + + public void addSongs(List songs) { + playingQueue.addAll(songs); + originalPlayingQueue.addAll(songs); + notifyChange(QUEUE_CHANGED); + } + + public void back(boolean force) { + if (getSongProgressMillis() > 2000) { + seek(0); + } else { + playPreviousSong(force); + } + } + + public void clearQueue() { + playingQueue.clear(); + originalPlayingQueue.clear(); + + setPosition(-1); + notifyChange(QUEUE_CHANGED); + } + + public void cycleRepeatMode() { + switch (getRepeatMode()) { + case REPEAT_MODE_NONE: + setRepeatMode(REPEAT_MODE_ALL); + break; + case REPEAT_MODE_ALL: + setRepeatMode(REPEAT_MODE_THIS); + break; + default: + setRepeatMode(REPEAT_MODE_NONE); + break; + } + } + + public int getAudioSessionId() { + if (playback != null) { + return playback.getAudioSessionId(); + } + return -1; + } + + @NonNull + public Song getCurrentSong() { + return getSongAt(getPosition()); + } + + @NonNull + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + public int getNextPosition(boolean force) { + int position = getPosition() + 1; + switch (getRepeatMode()) { + case REPEAT_MODE_ALL: + if (isLastTrack()) { + position = 0; } - try { - return bitmap.copy(config, false); - } catch (OutOfMemoryError e) { - e.printStackTrace(); - return null; - } - } - - private static String getTrackUri(@NonNull Song song) { - return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); - } - - @Override - public void onCreate() { - super.onCreate(); - final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); - } - - final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (powerManager != null) { - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); - } - wakeLock.setReferenceCounted(false); - - musicPlayerHandlerThread = new HandlerThread("PlaybackHandler"); - musicPlayerHandlerThread.start(); - playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); - - playback = new MultiPlayer(this); - playback.setCallbacks(this); - - setupMediaSession(); - - // queue saving needs to run on a separate thread so that it doesn't block the playback handler events - queueSaveHandlerThread = new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND); - queueSaveHandlerThread.start(); - queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper()); - - uiThreadHandler = new Handler(); - - registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE)); - registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); - registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); - - initNotification(); - - mediaStoreObserver = new MediaStoreObserver(this, playerHandler); - throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - - getContentResolver() - .registerContentObserver(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver(MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - - PreferenceUtil.INSTANCE.registerOnSharedPreferenceChangedListener(this); - - restoreState(); - - sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED")); - - registerHeadsetEvents(); - registerBluetoothConnected(); - } - - @Override - public void onDestroy() { - unregisterReceiver(widgetIntentReceiver); - unregisterReceiver(updateFavoriteReceiver); - unregisterReceiver(lockScreenReceiver); - if (becomingNoisyReceiverRegistered) { - unregisterReceiver(becomingNoisyReceiver); - becomingNoisyReceiverRegistered = false; - } - if (headsetReceiverRegistered) { - unregisterReceiver(headsetReceiver); - headsetReceiverRegistered = false; - } - if (bluetoothConnectedRegistered) { - unregisterReceiver(bluetoothReceiver); - bluetoothConnectedRegistered = false; - } - mediaSession.setActive(false); - quit(); - releaseResources(); - getContentResolver().unregisterContentObserver(mediaStoreObserver); - PreferenceUtil.INSTANCE.unregisterOnSharedPreferenceChangedListener(this); - wakeLock.release(); - - sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); - } - - public void acquireWakeLock(long milli) { - wakeLock.acquire(milli); - } - - public void addSong(int position, Song song) { - playingQueue.add(position, song); - originalPlayingQueue.add(position, song); - notifyChange(QUEUE_CHANGED); - } - - public void addSong(Song song) { - playingQueue.add(song); - originalPlayingQueue.add(song); - notifyChange(QUEUE_CHANGED); - } - - public void addSongs(int position, List songs) { - playingQueue.addAll(position, songs); - originalPlayingQueue.addAll(position, songs); - notifyChange(QUEUE_CHANGED); - } - - public void addSongs(List songs) { - playingQueue.addAll(songs); - originalPlayingQueue.addAll(songs); - notifyChange(QUEUE_CHANGED); - } - - public void back(boolean force) { - if (getSongProgressMillis() > 2000) { - seek(0); + break; + case REPEAT_MODE_THIS: + if (force) { + if (isLastTrack()) { + position = 0; + } } else { - playPreviousSong(force); + position -= 1; } - } - - public void clearQueue() { - playingQueue.clear(); - originalPlayingQueue.clear(); - - setPosition(-1); - notifyChange(QUEUE_CHANGED); - } - - public void cycleRepeatMode() { - switch (getRepeatMode()) { - case REPEAT_MODE_NONE: - setRepeatMode(REPEAT_MODE_ALL); - break; - case REPEAT_MODE_ALL: - setRepeatMode(REPEAT_MODE_THIS); - break; - default: - setRepeatMode(REPEAT_MODE_NONE); - break; + break; + default: + case REPEAT_MODE_NONE: + if (isLastTrack()) { + position -= 1; } + break; } + return position; + } - public int getAudioSessionId() { - if (playback != null) { - return playback.getAudioSessionId(); + @Nullable + public List getPlayingQueue() { + return playingQueue; + } + + public int getPosition() { + return position; + } + + public void setPosition(final int position) { + // handle this on the handlers thread to avoid blocking the ui thread + playerHandler.removeMessages(SET_POSITION); + playerHandler.obtainMessage(SET_POSITION, position, 0).sendToTarget(); + } + + public int getPreviousPosition(boolean force) { + int newPosition = getPosition() - 1; + switch (repeatMode) { + case REPEAT_MODE_ALL: + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } } - return -1; - } - - @NonNull - public Song getCurrentSong() { - return getSongAt(getPosition()); - } - - @NonNull - public MediaSessionCompat getMediaSession() { - return mediaSession; - } - - public int getNextPosition(boolean force) { - int position = getPosition() + 1; - switch (getRepeatMode()) { - case REPEAT_MODE_ALL: - if (isLastTrack()) { - position = 0; - } - break; - case REPEAT_MODE_THIS: - if (force) { - if (isLastTrack()) { - position = 0; - } - } else { - position -= 1; - } - break; - default: - case REPEAT_MODE_NONE: - if (isLastTrack()) { - position -= 1; - } - break; - } - return position; - } - - @Nullable - public List getPlayingQueue() { - return playingQueue; - } - - public int getPosition() { - return position; - } - - public void setPosition(final int position) { - // handle this on the handlers thread to avoid blocking the ui thread - playerHandler.removeMessages(SET_POSITION); - playerHandler.obtainMessage(SET_POSITION, position, 0).sendToTarget(); - } - - public int getPreviousPosition(boolean force) { - int newPosition = getPosition() - 1; - switch (repeatMode) { - case REPEAT_MODE_ALL: - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } - } - break; - case REPEAT_MODE_THIS: - if (force) { - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } - } - } else { - newPosition = getPosition(); - } - break; - default: - case REPEAT_MODE_NONE: - if (newPosition < 0) { - newPosition = 0; - } - break; - } - return newPosition; - } - - public long getQueueDurationMillis(int position) { - long duration = 0; - for (int i = position + 1; i < playingQueue.size(); i++) { - duration += playingQueue.get(i).getDuration(); - } - return duration; - } - - public int getRepeatMode() { - return repeatMode; - } - - public void setRepeatMode(final int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_NONE: - case REPEAT_MODE_ALL: - case REPEAT_MODE_THIS: - this.repeatMode = repeatMode; - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putInt(SAVED_REPEAT_MODE, repeatMode) - .apply(); - prepareNext(); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - break; - } - } - - public int getShuffleMode() { - return shuffleMode; - } - - public void setShuffleMode(final int shuffleMode) { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putInt(SAVED_SHUFFLE_MODE, shuffleMode) - .apply(); - switch (shuffleMode) { - case SHUFFLE_MODE_SHUFFLE: - this.shuffleMode = shuffleMode; - if (this.getPlayingQueue() != null) { - ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); - } - position = 0; - break; - case SHUFFLE_MODE_NONE: - this.shuffleMode = shuffleMode; - long currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); - playingQueue = new ArrayList<>(originalPlayingQueue); - int newPosition = 0; - if (getPlayingQueue() != null) { - for (Song song : getPlayingQueue()) { - if (song.getId() == currentSongId) { - newPosition = getPlayingQueue().indexOf(song); - } - } - } - position = newPosition; - break; - } - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - notifyChange(QUEUE_CHANGED); - } - - @NonNull - public Song getSongAt(int position) { - if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { - return getPlayingQueue().get(position); + break; + case REPEAT_MODE_THIS: + if (force) { + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } + } } else { - return Song.Companion.getEmptySong(); + newPosition = getPosition(); } - } - - public int getSongDurationMillis() { - if (playback != null) { - return playback.duration(); + break; + default: + case REPEAT_MODE_NONE: + if (newPosition < 0) { + newPosition = 0; } - return -1; + break; } + return newPosition; + } - public int getSongProgressMillis() { - if (playback != null) { - return playback.position(); + public long getQueueDurationMillis(int position) { + long duration = 0; + for (int i = position + 1; i < playingQueue.size(); i++) { + duration += playingQueue.get(i).getDuration(); + } + return duration; + } + + public int getRepeatMode() { + return repeatMode; + } + + public void setRepeatMode(final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_NONE: + case REPEAT_MODE_ALL: + case REPEAT_MODE_THIS: + this.repeatMode = repeatMode; + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_REPEAT_MODE, repeatMode) + .apply(); + prepareNext(); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + break; + } + } + + public int getShuffleMode() { + return shuffleMode; + } + + public void setShuffleMode(final int shuffleMode) { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_SHUFFLE_MODE, shuffleMode) + .apply(); + switch (shuffleMode) { + case SHUFFLE_MODE_SHUFFLE: + this.shuffleMode = shuffleMode; + if (this.getPlayingQueue() != null) { + ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); } - return -1; - } - - public void handleAndSendChangeInternal(@NonNull final String what) { - handleChangeInternal(what); - sendChangeInternal(what); - } - - public void initNotification() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - !PreferenceUtil.INSTANCE.isClassicNotification()) { - playingNotification = new PlayingNotificationImpl(); - } else { - playingNotification = new PlayingNotificationOreo(); - } - playingNotification.init(this); - } - - public boolean isLastTrack() { + position = 0; + break; + case SHUFFLE_MODE_NONE: + this.shuffleMode = shuffleMode; + long currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); + playingQueue = new ArrayList<>(originalPlayingQueue); + int newPosition = 0; if (getPlayingQueue() != null) { - return getPosition() == getPlayingQueue().size() - 1; - } - return false; - } - - public boolean isPausedByTransientLossOfFocus() { - return pausedByTransientLossOfFocus; - } - - public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { - this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; - } - - public boolean isPlaying() { - return playback != null && playback.isPlaying(); - } - - public void moveSong(int from, int to) { - if (from == to) { - return; - } - final int currentPosition = getPosition(); - Song songToMove = playingQueue.remove(from); - playingQueue.add(to, songToMove); - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - Song tmpSong = originalPlayingQueue.remove(from); - originalPlayingQueue.add(to, tmpSong); - } - if (from > currentPosition && to <= currentPosition) { - position = currentPosition + 1; - } else if (from < currentPosition && to >= currentPosition) { - position = currentPosition - 1; - } else if (from == currentPosition) { - position = to; - } - notifyChange(QUEUE_CHANGED); - } - - public void notifyChange(@NonNull final String what) { - handleAndSendChangeInternal(what); - sendPublicIntent(what); - } - - @NonNull - @Override - public IBinder onBind(Intent intent) { - return musicBind; - } - - @Override - public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, @NonNull String key) { - switch (key) { - case GAP_LESS_PLAYBACK: - if (sharedPreferences.getBoolean(key, false)) { - prepareNext(); - } else { - if (playback != null) { - playback.setNextDataSource(null); - } - } - break; - case ALBUM_ART_ON_LOCK_SCREEN: - case BLURRED_ALBUM_ART: - updateMediaSessionMetaData(); - break; - case COLORED_NOTIFICATION: - updateNotification(); - break; - case CLASSIC_NOTIFICATION: - initNotification(); - updateNotification(); - break; - case TOGGLE_HEADSET: - registerHeadsetEvents(); - break; - } - } - - @Override - public int onStartCommand(@Nullable Intent intent, int flags, int startId) { - if (intent != null && intent.getAction() != null) { - restoreQueuesAndPositionIfNecessary(); - String action = intent.getAction(); - switch (action) { - case ACTION_TOGGLE_PAUSE: - if (isPlaying()) { - pause(); - } else { - play(); - } - break; - case ACTION_PAUSE: - pause(); - break; - case ACTION_PLAY: - play(); - break; - case ACTION_PLAY_PLAYLIST: - playFromPlaylist(intent); - break; - case ACTION_REWIND: - back(true); - break; - case ACTION_SKIP: - playNextSong(true); - break; - case ACTION_STOP: - case ACTION_QUIT: - pendingQuit = false; - quit(); - break; - case ACTION_PENDING_QUIT: - pendingQuit = true; - break; - case TOGGLE_FAVORITE: - MusicUtil.INSTANCE.toggleFavorite(getApplicationContext(), getCurrentSong()); - break; + for (Song song : getPlayingQueue()) { + if (song.getId() == currentSongId) { + newPosition = getPlayingQueue().indexOf(song); } + } } - - return START_NOT_STICKY; + position = newPosition; + break; } + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + notifyChange(QUEUE_CHANGED); + } - @Override - public void onTrackEnded() { - acquireWakeLock(30000); - playerHandler.sendEmptyMessage(TRACK_ENDED); + @NonNull + public Song getSongAt(int position) { + if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { + return getPlayingQueue().get(position); + } else { + return Song.Companion.getEmptySong(); } + } - @Override - public void onTrackWentToNext() { - playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); + public int getSongDurationMillis() { + if (playback != null) { + return playback.duration(); } + return -1; + } - @Override - public boolean onUnbind(Intent intent) { - if (!isPlaying()) { - stopSelf(); + public int getSongProgressMillis() { + if (playback != null) { + return playback.position(); + } + return -1; + } + + public void handleAndSendChangeInternal(@NonNull final String what) { + handleChangeInternal(what); + sendChangeInternal(what); + } + + public void initNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && !PreferenceUtil.INSTANCE.isClassicNotification()) { + playingNotification = new PlayingNotificationImpl(); + } else { + playingNotification = new PlayingNotificationOreo(); + } + playingNotification.init(this); + } + + public boolean isLastTrack() { + if (getPlayingQueue() != null) { + return getPosition() == getPlayingQueue().size() - 1; + } + return false; + } + + public boolean isPausedByTransientLossOfFocus() { + return pausedByTransientLossOfFocus; + } + + public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { + this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; + } + + public boolean isPlaying() { + return playback != null && playback.isPlaying(); + } + + public void moveSong(int from, int to) { + if (from == to) { + return; + } + final int currentPosition = getPosition(); + Song songToMove = playingQueue.remove(from); + playingQueue.add(to, songToMove); + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + Song tmpSong = originalPlayingQueue.remove(from); + originalPlayingQueue.add(to, tmpSong); + } + if (from > currentPosition && to <= currentPosition) { + position = currentPosition + 1; + } else if (from < currentPosition && to >= currentPosition) { + position = currentPosition - 1; + } else if (from == currentPosition) { + position = to; + } + notifyChange(QUEUE_CHANGED); + } + + public void notifyChange(@NonNull final String what) { + handleAndSendChangeInternal(what); + sendPublicIntent(what); + } + + @NonNull + @Override + public IBinder onBind(Intent intent) { + return musicBind; + } + + @Override + public void onSharedPreferenceChanged( + @NonNull SharedPreferences sharedPreferences, @NonNull String key) { + switch (key) { + case GAP_LESS_PLAYBACK: + if (sharedPreferences.getBoolean(key, false)) { + prepareNext(); + } else { + if (playback != null) { + playback.setNextDataSource(null); + } } - return true; + break; + case ALBUM_ART_ON_LOCK_SCREEN: + case BLURRED_ALBUM_ART: + updateMediaSessionMetaData(); + break; + case COLORED_NOTIFICATION: + updateNotification(); + break; + case CLASSIC_NOTIFICATION: + initNotification(); + updateNotification(); + break; + case TOGGLE_HEADSET: + registerHeadsetEvents(); + break; + } + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + restoreQueuesAndPositionIfNecessary(); + String action = intent.getAction(); + switch (action) { + case ACTION_TOGGLE_PAUSE: + if (isPlaying()) { + pause(); + } else { + play(); + } + break; + case ACTION_PAUSE: + pause(); + break; + case ACTION_PLAY: + play(); + break; + case ACTION_PLAY_PLAYLIST: + playFromPlaylist(intent); + break; + case ACTION_REWIND: + back(true); + break; + case ACTION_SKIP: + playNextSong(true); + break; + case ACTION_STOP: + case ACTION_QUIT: + pendingQuit = false; + quit(); + break; + case ACTION_PENDING_QUIT: + pendingQuit = true; + break; + case TOGGLE_FAVORITE: + MusicUtil.INSTANCE.toggleFavorite(getApplicationContext(), getCurrentSong()); + break; + } } - public void openQueue(@Nullable final List playingQueue, final int startPosition, - final boolean startPlaying) { - if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue - .size()) { - // it is important to copy the playing queue here first as we might add/remove songs later - originalPlayingQueue = new ArrayList<>(playingQueue); - this.playingQueue = new ArrayList<>(originalPlayingQueue); + return START_NOT_STICKY; + } - int position = startPosition; - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - ShuffleHelper.INSTANCE.makeShuffleList(this.playingQueue, startPosition); - position = 0; + @Override + public void onTrackEnded() { + acquireWakeLock(30000); + playerHandler.sendEmptyMessage(TRACK_ENDED); + } + + @Override + public void onTrackWentToNext() { + playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); + } + + @Override + public boolean onUnbind(Intent intent) { + if (!isPlaying()) { + stopSelf(); + } + return true; + } + + public void openQueue( + @Nullable final List playingQueue, + final int startPosition, + final boolean startPlaying) { + if (playingQueue != null + && !playingQueue.isEmpty() + && startPosition >= 0 + && startPosition < playingQueue.size()) { + // it is important to copy the playing queue here first as we might add/remove songs later + originalPlayingQueue = new ArrayList<>(playingQueue); + this.playingQueue = new ArrayList<>(originalPlayingQueue); + + int position = startPosition; + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + ShuffleHelper.INSTANCE.makeShuffleList(this.playingQueue, startPosition); + position = 0; + } + if (startPlaying) { + playSongAt(position); + } else { + setPosition(position); + } + notifyChange(QUEUE_CHANGED); + } + } + + public boolean openTrackAndPrepareNextAt(int position) { + synchronized (this) { + this.position = position; + boolean prepared = openCurrent(); + if (prepared) { + prepareNextImpl(); + } + notifyChange(META_CHANGED); + notHandledMetaChangedForCurrentTrack = false; + return prepared; + } + } + + public void pause() { + pausedByTransientLossOfFocus = false; + if (playback != null && playback.isPlaying()) { + playback.pause(); + notifyChange(PLAY_STATE_CHANGED); + } + } + + public void play() { + synchronized (this) { + if (requestFocus()) { + if (playback != null && !playback.isPlaying()) { + if (!playback.isInitialized()) { + playSongAt(getPosition()); + } else { + playback.start(); + if (!becomingNoisyReceiverRegistered) { + registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter); + becomingNoisyReceiverRegistered = true; } - if (startPlaying) { - playSongAt(position); - } else { - setPosition(position); + if (notHandledMetaChangedForCurrentTrack) { + handleChangeInternal(META_CHANGED); + notHandledMetaChangedForCurrentTrack = false; } - notifyChange(QUEUE_CHANGED); - } - } - - public boolean openTrackAndPrepareNextAt(int position) { - synchronized (this) { - this.position = position; - boolean prepared = openCurrent(); - if (prepared) { - prepareNextImpl(); - } - notifyChange(META_CHANGED); - notHandledMetaChangedForCurrentTrack = false; - return prepared; - } - } - - public void pause() { - pausedByTransientLossOfFocus = false; - if (playback != null && playback.isPlaying()) { - playback.pause(); notifyChange(PLAY_STATE_CHANGED); + + // fixes a bug where the volume would stay ducked because the + // AudioManager.AUDIOFOCUS_GAIN event is not sent + playerHandler.removeMessages(DUCK); + playerHandler.sendEmptyMessage(UNDUCK); + } } + } else { + Toast.makeText( + this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT) + .show(); + } + } + } + + public void playNextSong(boolean force) { + playSongAt(getNextPosition(force)); + } + + public void playPreviousSong(boolean force) { + playSongAt(getPreviousPosition(force)); + } + + public void playSongAt(final int position) { + // handle this on the handlers thread to avoid blocking the ui thread + playerHandler.removeMessages(PLAY_SONG); + playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); + } + + public void playSongAtImpl(int position) { + if (openTrackAndPrepareNextAt(position)) { + play(); + } else { + Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT) + .show(); + } + } + + public void playSongs(ArrayList songs, int shuffleMode) { + if (songs != null && !songs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = new Random().nextInt(songs.size()); + openQueue(songs, startPosition, false); + setShuffleMode(shuffleMode); + } else { + openQueue(songs, 0, false); + } + play(); + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); + } + } + + public boolean prepareNextImpl() { + synchronized (this) { + try { + int nextPosition = getNextPosition(false); + if (playback != null) { + playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); + } + this.nextPosition = nextPosition; + return true; + } catch (Exception e) { + return false; + } + } + } + + public void quit() { + pause(); + playingNotification.stop(); + + closeAudioEffectSession(); + getAudioManager().abandonAudioFocus(audioFocusListener); + stopSelf(); + } + + public void releaseWakeLock() { + if (wakeLock.isHeld()) { + wakeLock.release(); + } + } + + public void removeSong(int position) { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + playingQueue.remove(position); + originalPlayingQueue.remove(position); + } else { + originalPlayingQueue.remove(playingQueue.remove(position)); } - public void play() { - synchronized (this) { - if (requestFocus()) { - if (playback != null && !playback.isPlaying()) { - if (!playback.isInitialized()) { - playSongAt(getPosition()); - } else { - playback.start(); - if (!becomingNoisyReceiverRegistered) { - registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter); - becomingNoisyReceiverRegistered = true; - } - if (notHandledMetaChangedForCurrentTrack) { - handleChangeInternal(META_CHANGED); - notHandledMetaChangedForCurrentTrack = false; - } - notifyChange(PLAY_STATE_CHANGED); + rePosition(position); - // fixes a bug where the volume would stay ducked because the AudioManager.AUDIOFOCUS_GAIN event is not sent - playerHandler.removeMessages(DUCK); - playerHandler.sendEmptyMessage(UNDUCK); + notifyChange(QUEUE_CHANGED); + } + + public void removeSong(@NonNull Song song) { + for (int i = 0; i < playingQueue.size(); i++) { + if (playingQueue.get(i).getId() == song.getId()) { + playingQueue.remove(i); + rePosition(i); + } + } + for (int i = 0; i < originalPlayingQueue.size(); i++) { + if (originalPlayingQueue.get(i).getId() == song.getId()) { + originalPlayingQueue.remove(i); + } + } + notifyChange(QUEUE_CHANGED); + } + + public synchronized void restoreQueuesAndPositionIfNecessary() { + if (!queuesRestored && playingQueue.isEmpty()) { + List restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); + List restoredOriginalQueue = + MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); + int restoredPosition = + PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); + int restoredPositionInTrack = + PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION_IN_TRACK, -1); + + if (restoredQueue.size() > 0 + && restoredQueue.size() == restoredOriginalQueue.size() + && restoredPosition != -1) { + this.originalPlayingQueue = restoredOriginalQueue; + this.playingQueue = restoredQueue; + + position = restoredPosition; + openCurrent(); + prepareNext(); + + if (restoredPositionInTrack > 0) { + seek(restoredPositionInTrack); + } + + notHandledMetaChangedForCurrentTrack = true; + sendChangeInternal(META_CHANGED); + sendChangeInternal(QUEUE_CHANGED); + } + } + queuesRestored = true; + } + + public void runOnUiThread(Runnable runnable) { + uiThreadHandler.post(runnable); + } + + public void savePositionInTrack() { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()) + .apply(); + } + + public void saveQueuesImpl() { + MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); + } + + public void saveState() { + saveQueues(); + savePosition(); + savePositionInTrack(); + } + + public int seek(int millis) { + synchronized (this) { + try { + int newPosition = 0; + if (playback != null) { + newPosition = playback.seek(millis); + } + throttledSeekHandler.notifySeek(); + return newPosition; + } catch (Exception e) { + return -1; + } + } + } + + // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch + public void sendPublicIntent(@NonNull final String what) { + final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); + + final Song song = getCurrentSong(); + + if (song != null) { + intent.putExtra("id", song.getId()); + intent.putExtra("artist", song.getArtistName()); + intent.putExtra("album", song.getAlbumName()); + intent.putExtra("track", song.getTitle()); + intent.putExtra("duration", song.getDuration()); + intent.putExtra("position", (long) getSongProgressMillis()); + intent.putExtra("playing", isPlaying()); + intent.putExtra("scrobbling_source", RETRO_MUSIC_PACKAGE_NAME); + sendStickyBroadcast(intent); + } + } + + public void toggleShuffle() { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + setShuffleMode(SHUFFLE_MODE_SHUFFLE); + } else { + setShuffleMode(SHUFFLE_MODE_NONE); + } + } + + public void updateMediaSessionPlaybackState() { + PlaybackStateCompat.Builder stateBuilder = + new PlaybackStateCompat.Builder() + .setActions(MEDIA_SESSION_ACTIONS) + .setState( + isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, + getSongProgressMillis(), + 1); + + setCustomAction(stateBuilder); + + mediaSession.setPlaybackState(stateBuilder.build()); + } + + public void updateNotification() { + if (playingNotification != null && getCurrentSong().getId() != -1) { + playingNotification.update(); + } + } + + void updateMediaSessionMetaData() { + final Song song = getCurrentSong(); + + if (song.getId() == -1) { + mediaSession.setMetadata(null); + return; + } + + final MediaMetadataCompat.Builder metaData = + new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtistName()) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtistName()) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbumName()) + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration()) + .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getPosition() + 1) + .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.getYear()) + .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null) + .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); + + if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) { + final Point screenSize = RetroUtil.getScreenSize(MusicService.this); + final BitmapRequestBuilder request = + SongGlideRequest.Builder.from(Glide.with(MusicService.this), song) + .checkIgnoreMediaStore(MusicService.this) + .asBitmap() + .build(); + if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) { + request.transform(new BlurTransformation.Builder(MusicService.this).build()); + } + runOnUiThread( + new Runnable() { + @Override + public void run() { + request.into( + new SimpleTarget(screenSize.x, screenSize.y) { + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + super.onLoadFailed(e, errorDrawable); + mediaSession.setMetadata(metaData.build()); } - } - } else { - Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT) - .show(); + + @Override + public void onResourceReady( + Bitmap resource, GlideAnimation glideAnimation) { + metaData.putBitmap( + MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); + mediaSession.setMetadata(metaData.build()); + } + }); } + }); + } else { + mediaSession.setMetadata(metaData.build()); + } + } + + private void closeAudioEffectSession() { + final Intent audioEffectsIntent = + new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + if (playback != null) { + audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); + } + audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(audioEffectsIntent); + } + + private AudioManager getAudioManager() { + if (audioManager == null) { + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + return audioManager; + } + + private void handleChangeInternal(@NonNull final String what) { + switch (what) { + case PLAY_STATE_CHANGED: + updateNotification(); + updateMediaSessionPlaybackState(); + final boolean isPlaying = isPlaying(); + if (!isPlaying && getSongProgressMillis() > 0) { + savePositionInTrack(); } - } - - public void playNextSong(boolean force) { - playSongAt(getNextPosition(force)); - } - - public void playPreviousSong(boolean force) { - playSongAt(getPreviousPosition(force)); - } - - public void playSongAt(final int position) { - // handle this on the handlers thread to avoid blocking the ui thread - playerHandler.removeMessages(PLAY_SONG); - playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); - } - - public void playSongAtImpl(int position) { - if (openTrackAndPrepareNextAt(position)) { - play(); - } else { - Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); - } - } - - public void playSongs(ArrayList songs, int shuffleMode) { - if (songs != null && !songs.isEmpty()) { - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - int startPosition = new Random().nextInt(songs.size()); - openQueue(songs, startPosition, false); - setShuffleMode(shuffleMode); - } else { - openQueue(songs, 0, false); - } - play(); - } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); - } - } - - public boolean prepareNextImpl() { - synchronized (this) { - try { - int nextPosition = getNextPosition(false); - if (playback != null) { - playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); - } - this.nextPosition = nextPosition; - return true; - } catch (Exception e) { - return false; - } - } - } - - public void quit() { - pause(); - playingNotification.stop(); - - closeAudioEffectSession(); - getAudioManager().abandonAudioFocus(audioFocusListener); - stopSelf(); - } - - public void releaseWakeLock() { - if (wakeLock.isHeld()) { - wakeLock.release(); - } - } - - public void removeSong(int position) { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - playingQueue.remove(position); - originalPlayingQueue.remove(position); - } else { - originalPlayingQueue.remove(playingQueue.remove(position)); - } - - rePosition(position); - - notifyChange(QUEUE_CHANGED); - } - - public void removeSong(@NonNull Song song) { - for (int i = 0; i < playingQueue.size(); i++) { - if (playingQueue.get(i).getId() == song.getId()) { - playingQueue.remove(i); - rePosition(i); - } - } - for (int i = 0; i < originalPlayingQueue.size(); i++) { - if (originalPlayingQueue.get(i).getId() == song.getId()) { - originalPlayingQueue.remove(i); - } - } - notifyChange(QUEUE_CHANGED); - } - - public synchronized void restoreQueuesAndPositionIfNecessary() { - if (!queuesRestored && playingQueue.isEmpty()) { - List restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); - List restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); - int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); - int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this) - .getInt(SAVED_POSITION_IN_TRACK, -1); - - if (restoredQueue.size() > 0 && restoredQueue.size() == restoredOriginalQueue.size() - && restoredPosition != -1) { - this.originalPlayingQueue = restoredOriginalQueue; - this.playingQueue = restoredQueue; - - position = restoredPosition; - openCurrent(); - prepareNext(); - - if (restoredPositionInTrack > 0) { - seek(restoredPositionInTrack); - } - - notHandledMetaChangedForCurrentTrack = true; - sendChangeInternal(META_CHANGED); - sendChangeInternal(QUEUE_CHANGED); - } - } - queuesRestored = true; - } - - public void runOnUiThread(Runnable runnable) { - uiThreadHandler.post(runnable); - } - - public void savePositionInTrack() { - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()).apply(); - } - - public void saveQueuesImpl() { - MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); - } - - public void saveState() { - saveQueues(); + songPlayCountHelper.notifyPlayStateChanged(isPlaying); + break; + case FAVORITE_STATE_CHANGED: + case META_CHANGED: + updateNotification(); + updateMediaSessionMetaData(); savePosition(); savePositionInTrack(); - } - - public int seek(int millis) { - synchronized (this) { - try { - int newPosition = 0; - if (playback != null) { - newPosition = playback.seek(millis); - } - throttledSeekHandler.notifySeek(); - return newPosition; - } catch (Exception e) { - return -1; - } + final Song currentSong = getCurrentSong(); + if (currentSong != null) { + HistoryStore.getInstance(this).addSongId(currentSong.getId()); } - } - - // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch - public void sendPublicIntent(@NonNull final String what) { - final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); - - final Song song = getCurrentSong(); - - if (song != null) { - intent.putExtra("id", song.getId()); - intent.putExtra("artist", song.getArtistName()); - intent.putExtra("album", song.getAlbumName()); - intent.putExtra("track", song.getTitle()); - intent.putExtra("duration", song.getDuration()); - intent.putExtra("position", (long) getSongProgressMillis()); - intent.putExtra("playing", isPlaying()); - intent.putExtra("scrobbling_source", RETRO_MUSIC_PACKAGE_NAME); - sendStickyBroadcast(intent); + if (songPlayCountHelper.shouldBumpPlayCount()) { + SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId()); } - } - - public void toggleShuffle() { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - setShuffleMode(SHUFFLE_MODE_SHUFFLE); + if (currentSong != null) { + songPlayCountHelper.notifySongChanged(currentSong); + } + break; + case QUEUE_CHANGED: + updateMediaSessionMetaData(); // because playing queue size might have changed + saveState(); + if (playingQueue.size() > 0) { + prepareNext(); } else { - setShuffleMode(SHUFFLE_MODE_NONE); + playingNotification.stop(); } + break; } + } - public void updateMediaSessionPlaybackState() { - PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder() - .setActions(MEDIA_SESSION_ACTIONS) - .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, - getSongProgressMillis(), 1); - - setCustomAction(stateBuilder); - - mediaSession.setPlaybackState(stateBuilder.build()); - } - - public void updateNotification() { - if (playingNotification != null && getCurrentSong().getId() != -1) { - playingNotification.update(); - } - } - - void updateMediaSessionMetaData() { - final Song song = getCurrentSong(); - - if (song.getId() == -1) { - mediaSession.setMetadata(null); - return; - } - - final MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtistName()) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtistName()) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbumName()) - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()) - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration()) - .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getPosition() + 1) - .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.getYear()) - .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null) - .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); - - if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) { - final Point screenSize = RetroUtil.getScreenSize(MusicService.this); - final BitmapRequestBuilder request = SongGlideRequest.Builder - .from(Glide.with(MusicService.this), song) - .checkIgnoreMediaStore(MusicService.this) - .asBitmap().build(); - if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) { - request.transform(new BlurTransformation.Builder(MusicService.this).build()); - } - runOnUiThread(new Runnable() { - @Override - public void run() { - request.into(new SimpleTarget(screenSize.x, screenSize.y) { - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - super.onLoadFailed(e, errorDrawable); - mediaSession.setMetadata(metaData.build()); - } - - @Override - public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - metaData.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); - mediaSession.setMetadata(metaData.build()); - } - }); - } - }); - } else { - mediaSession.setMetadata(metaData.build()); - } - } - - private void closeAudioEffectSession() { - final Intent audioEffectsIntent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + private boolean openCurrent() { + synchronized (this) { + try { if (playback != null) { - audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); - } - audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(audioEffectsIntent); - } - - private AudioManager getAudioManager() { - if (audioManager == null) { - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - } - return audioManager; - } - - private void handleChangeInternal(@NonNull final String what) { - switch (what) { - case PLAY_STATE_CHANGED: - updateNotification(); - updateMediaSessionPlaybackState(); - final boolean isPlaying = isPlaying(); - if (!isPlaying && getSongProgressMillis() > 0) { - savePositionInTrack(); - } - songPlayCountHelper.notifyPlayStateChanged(isPlaying); - break; - case FAVORITE_STATE_CHANGED: - case META_CHANGED: - updateNotification(); - updateMediaSessionMetaData(); - savePosition(); - savePositionInTrack(); - final Song currentSong = getCurrentSong(); - if (currentSong != null) { - HistoryStore.getInstance(this).addSongId(currentSong.getId()); - } - if (songPlayCountHelper.shouldBumpPlayCount()) { - SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId()); - } - if (currentSong != null) { - songPlayCountHelper.notifySongChanged(currentSong); - } - break; - case QUEUE_CHANGED: - updateMediaSessionMetaData(); // because playing queue size might have changed - saveState(); - if (playingQueue.size() > 0) { - prepareNext(); - } else { - playingNotification.stop(); - } - break; - } - } - - private boolean openCurrent() { - synchronized (this) { - try { - if (playback != null) { - return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); - } - } catch (Exception e) { - return false; - } + return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); } + } catch (Exception e) { return false; + } } + return false; + } - private void playFromPlaylist(Intent intent) { - Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); - int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); - if (playlist != null) { - List playlistSongs = playlist.getSongs(); - if (!playlistSongs.isEmpty()) { - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - int startPosition = new Random().nextInt(playlistSongs.size()); - openQueue(playlistSongs, startPosition, true); - setShuffleMode(shuffleMode); - } else { - openQueue(playlistSongs, 0, true); - } - } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); - } + private void playFromPlaylist(Intent intent) { + Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); + int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); + if (playlist != null) { + List playlistSongs = playlist.getSongs(); + if (!playlistSongs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = new Random().nextInt(playlistSongs.size()); + openQueue(playlistSongs, startPosition, true); + setShuffleMode(shuffleMode); } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); + openQueue(playlistSongs, 0, true); } + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG) + .show(); + } + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); } + } - private void prepareNext() { - playerHandler.removeMessages(PREPARE_NEXT); - playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); + private void prepareNext() { + playerHandler.removeMessages(PREPARE_NEXT); + playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); + } + + private void rePosition(int deletedPosition) { + int currentPosition = getPosition(); + if (deletedPosition < currentPosition) { + position = currentPosition - 1; + } else if (deletedPosition == currentPosition) { + if (playingQueue.size() > deletedPosition) { + setPosition(position); + } else { + setPosition(position - 1); + } } + } - private void rePosition(int deletedPosition) { - int currentPosition = getPosition(); - if (deletedPosition < currentPosition) { - position = currentPosition - 1; - } else if (deletedPosition == currentPosition) { - if (playingQueue.size() > deletedPosition) { - setPosition(position); - } else { - setPosition(position - 1); - } - } + private void registerBluetoothConnected() { + Log.i(TAG, "registerBluetoothConnected: "); + if (!bluetoothConnectedRegistered) { + registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter); + bluetoothConnectedRegistered = true; } + } - private void registerBluetoothConnected() { - Log.i(TAG, "registerBluetoothConnected: "); - if (!bluetoothConnectedRegistered) { - registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter); - bluetoothConnectedRegistered = true; - } + private void registerHeadsetEvents() { + if (!headsetReceiverRegistered && PreferenceUtil.INSTANCE.isHeadsetPlugged()) { + registerReceiver(headsetReceiver, headsetReceiverIntentFilter); + headsetReceiverRegistered = true; } + } - private void registerHeadsetEvents() { - if (!headsetReceiverRegistered && PreferenceUtil.INSTANCE.isHeadsetPlugged()) { - registerReceiver(headsetReceiver, headsetReceiverIntentFilter); - headsetReceiverRegistered = true; - } + private void releaseResources() { + playerHandler.removeCallbacksAndMessages(null); + musicPlayerHandlerThread.quitSafely(); + queueSaveHandler.removeCallbacksAndMessages(null); + queueSaveHandlerThread.quitSafely(); + if (playback != null) { + playback.release(); } + playback = null; + mediaSession.release(); + } - private void releaseResources() { - playerHandler.removeCallbacksAndMessages(null); - musicPlayerHandlerThread.quitSafely(); - queueSaveHandler.removeCallbacksAndMessages(null); - queueSaveHandlerThread.quitSafely(); - if (playback != null) { - playback.release(); - } - playback = null; - mediaSession.release(); + private boolean requestFocus() { + return (getAudioManager() + .requestAudioFocus( + audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + } + + private void restoreState() { + shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); + repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + + playerHandler.removeMessages(RESTORE_QUEUES); + playerHandler.sendEmptyMessage(RESTORE_QUEUES); + } + + private void savePosition() { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_POSITION, getPosition()) + .apply(); + } + + private void saveQueues() { + queueSaveHandler.removeMessages(SAVE_QUEUES); + queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); + } + + private void sendChangeInternal(final String what) { + sendBroadcast(new Intent(what)); + appWidgetBig.notifyChange(this, what); + appWidgetClassic.notifyChange(this, what); + appWidgetSmall.notifyChange(this, what); + appWidgetCard.notifyChange(this, what); + appWidgetText.notifyChange(this, what); + } + + private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { + int repeatIcon = R.drawable.ic_repeat; // REPEAT_MODE_NONE + if (getRepeatMode() == REPEAT_MODE_THIS) { + repeatIcon = R.drawable.ic_repeat_one; + } else if (getRepeatMode() == REPEAT_MODE_ALL) { + repeatIcon = R.drawable.ic_repeat_white_circle; } - - private boolean requestFocus() { - return (getAudioManager() - .requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) - == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - } - - private void restoreState() { - shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); - repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - - playerHandler.removeMessages(RESTORE_QUEUES); - playerHandler.sendEmptyMessage(RESTORE_QUEUES); - } - - private void savePosition() { - PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION, getPosition()).apply(); - } - - private void saveQueues() { - queueSaveHandler.removeMessages(SAVE_QUEUES); - queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); - } - - private void sendChangeInternal(final String what) { - sendBroadcast(new Intent(what)); - appWidgetBig.notifyChange(this, what); - appWidgetClassic.notifyChange(this, what); - appWidgetSmall.notifyChange(this, what); - appWidgetCard.notifyChange(this, what); - appWidgetText.notifyChange(this, what); - } - - private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { - int repeatIcon = R.drawable.ic_repeat; // REPEAT_MODE_NONE - if (getRepeatMode() == REPEAT_MODE_THIS) { - repeatIcon = R.drawable.ic_repeat_one; - } else if (getRepeatMode() == REPEAT_MODE_ALL) { - repeatIcon = R.drawable.ic_repeat_white_circle; - } - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( CYCLE_REPEAT, getString(R.string.action_cycle_repeat), repeatIcon) - .build()); + .build()); - final int shuffleIcon = getShuffleMode() == SHUFFLE_MODE_NONE ? R.drawable.ic_shuffle_off_circled - : R.drawable.ic_shuffle_on_circled; - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + final int shuffleIcon = + getShuffleMode() == SHUFFLE_MODE_NONE + ? R.drawable.ic_shuffle_off_circled + : R.drawable.ic_shuffle_on_circled; + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( TOGGLE_SHUFFLE, getString(R.string.action_toggle_shuffle), shuffleIcon) - .build()); + .build()); - final int favoriteIcon = MusicUtil.INSTANCE.isFavorite(getApplicationContext(), getCurrentSong()) - ? R.drawable.ic_favorite : R.drawable.ic_favorite_border; - stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder( + final int favoriteIcon = + MusicUtil.INSTANCE.isFavorite(getApplicationContext(), getCurrentSong()) + ? R.drawable.ic_favorite + : R.drawable.ic_favorite_border; + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( TOGGLE_FAVORITE, getString(R.string.action_toggle_favorite), favoriteIcon) - .build()); + .build()); + } + + private void setupMediaSession() { + ComponentName mediaButtonReceiverComponentName = + new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class); + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); + + PendingIntent mediaButtonReceiverPendingIntent = + PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); + + mediaSession = + new MediaSessionCompat( + this, + "RetroMusicPlayer", + mediaButtonReceiverComponentName, + mediaButtonReceiverPendingIntent); + MediaSessionCallback mediasessionCallback = + new MediaSessionCallback(getApplicationContext(), this); + mediaSession.setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setCallback(mediasessionCallback); + mediaSession.setActive(true); + mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); + } + + public class MusicBinder extends Binder { + + @NonNull + public MusicService getService() { + return MusicService.this; } - - private void setupMediaSession() { - ComponentName mediaButtonReceiverComponentName = new ComponentName( - getApplicationContext(), - MediaButtonIntentReceiver.class); - - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); - - PendingIntent mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), - 0, - mediaButtonIntent, - 0); - - mediaSession = new MediaSessionCompat(this, - "RetroMusicPlayer", - mediaButtonReceiverComponentName, - mediaButtonReceiverPendingIntent); - MediaSessionCallback mediasessionCallback = new MediaSessionCallback( - getApplicationContext(), this); - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS - ); - mediaSession.setCallback(mediasessionCallback); - mediaSession.setActive(true); - mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); - - } - - public class MusicBinder extends Binder { - - @NonNull - public MusicService getService() { - return MusicService.this; - } - } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java index 39b1258ce..5928a5337 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java @@ -14,17 +14,6 @@ package code.name.monkey.retromusic.service; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; - -import androidx.annotation.NonNull; - -import java.lang.ref.WeakReference; - -import code.name.monkey.retromusic.util.PreferenceUtil; - import static code.name.monkey.retromusic.service.MusicService.DUCK; import static code.name.monkey.retromusic.service.MusicService.META_CHANGED; import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED; @@ -32,140 +21,148 @@ import static code.name.monkey.retromusic.service.MusicService.REPEAT_MODE_NONE; import static code.name.monkey.retromusic.service.MusicService.TRACK_ENDED; import static code.name.monkey.retromusic.service.MusicService.TRACK_WENT_TO_NEXT; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.NonNull; +import code.name.monkey.retromusic.util.PreferenceUtil; +import java.lang.ref.WeakReference; + class PlaybackHandler extends Handler { - @NonNull - private final WeakReference mService; - private float currentDuckVolume = 1.0f; + @NonNull private final WeakReference mService; + private float currentDuckVolume = 1.0f; - PlaybackHandler(final MusicService service, @NonNull final Looper looper) { - super(looper); - mService = new WeakReference<>(service); + PlaybackHandler(final MusicService service, @NonNull final Looper looper) { + super(looper); + mService = new WeakReference<>(service); + } + + @Override + public void handleMessage(@NonNull final Message msg) { + final MusicService service = mService.get(); + if (service == null) { + return; } - @Override - public void handleMessage(@NonNull final Message msg) { - final MusicService service = mService.get(); - if (service == null) { - return; + switch (msg.what) { + case MusicService.DUCK: + if (PreferenceUtil.INSTANCE.isAudioDucking()) { + currentDuckVolume -= .05f; + if (currentDuckVolume > .2f) { + sendEmptyMessageDelayed(DUCK, 10); + } else { + currentDuckVolume = .2f; + } + } else { + currentDuckVolume = 1f; } + service.playback.setVolume(currentDuckVolume); + break; - switch (msg.what) { - case MusicService.DUCK: - if (PreferenceUtil.INSTANCE.isAudioDucking()) { - currentDuckVolume -= .05f; - if (currentDuckVolume > .2f) { - sendEmptyMessageDelayed(DUCK, 10); - } else { - currentDuckVolume = .2f; - } - } else { - currentDuckVolume = 1f; - } - service.playback.setVolume(currentDuckVolume); - break; - - case MusicService.UNDUCK: - if (PreferenceUtil.INSTANCE.isAudioDucking()) { - currentDuckVolume += .03f; - if (currentDuckVolume < 1f) { - sendEmptyMessageDelayed(MusicService.UNDUCK, 10); - } else { - currentDuckVolume = 1f; - } - } else { - currentDuckVolume = 1f; - } - service.playback.setVolume(currentDuckVolume); - break; - - case TRACK_WENT_TO_NEXT: - if (service.pendingQuit || service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { - service.pause(); - service.seek(0); - if (service.pendingQuit) { - service.pendingQuit = false; - service.quit(); - break; - } - } else { - service.position = service.nextPosition; - service.prepareNextImpl(); - service.notifyChange(META_CHANGED); - } - break; - - case TRACK_ENDED: - // if there is a timer finished, don't continue - if (service.pendingQuit || - service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { - service.notifyChange(PLAY_STATE_CHANGED); - service.seek(0); - if (service.pendingQuit) { - service.pendingQuit = false; - service.quit(); - break; - } - } else { - service.playNextSong(false); - } - sendEmptyMessage(MusicService.RELEASE_WAKELOCK); - break; - - case MusicService.RELEASE_WAKELOCK: - service.releaseWakeLock(); - break; - - case MusicService.PLAY_SONG: - service.playSongAtImpl(msg.arg1); - break; - - case MusicService.SET_POSITION: - service.openTrackAndPrepareNextAt(msg.arg1); - service.notifyChange(PLAY_STATE_CHANGED); - break; - - case MusicService.PREPARE_NEXT: - service.prepareNextImpl(); - break; - - case MusicService.RESTORE_QUEUES: - service.restoreQueuesAndPositionIfNecessary(); - break; - - case MusicService.FOCUS_CHANGE: - switch (msg.arg1) { - case AudioManager.AUDIOFOCUS_GAIN: - if (!service.isPlaying() && service.isPausedByTransientLossOfFocus()) { - service.play(); - service.setPausedByTransientLossOfFocus(false); - } - removeMessages(DUCK); - sendEmptyMessage(MusicService.UNDUCK); - break; - - case AudioManager.AUDIOFOCUS_LOSS: - // Lost focus for an unbounded amount of time: stop playback and release media playback - service.pause(); - break; - - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - // Lost focus for a short time, but we have to stop - // playback. We don't release the media playback because playback - // is likely to resume - boolean wasPlaying = service.isPlaying(); - service.pause(); - service.setPausedByTransientLossOfFocus(wasPlaying); - break; - - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - // Lost focus for a short time, but it's ok to keep playing - // at an attenuated level - removeMessages(MusicService.UNDUCK); - sendEmptyMessage(DUCK); - break; - } - break; + case MusicService.UNDUCK: + if (PreferenceUtil.INSTANCE.isAudioDucking()) { + currentDuckVolume += .03f; + if (currentDuckVolume < 1f) { + sendEmptyMessageDelayed(MusicService.UNDUCK, 10); + } else { + currentDuckVolume = 1f; + } + } else { + currentDuckVolume = 1f; } + service.playback.setVolume(currentDuckVolume); + break; + + case TRACK_WENT_TO_NEXT: + if (service.pendingQuit + || service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { + service.pause(); + service.seek(0); + if (service.pendingQuit) { + service.pendingQuit = false; + service.quit(); + break; + } + } else { + service.position = service.nextPosition; + service.prepareNextImpl(); + service.notifyChange(META_CHANGED); + } + break; + + case TRACK_ENDED: + // if there is a timer finished, don't continue + if (service.pendingQuit + || service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { + service.notifyChange(PLAY_STATE_CHANGED); + service.seek(0); + if (service.pendingQuit) { + service.pendingQuit = false; + service.quit(); + break; + } + } else { + service.playNextSong(false); + } + sendEmptyMessage(MusicService.RELEASE_WAKELOCK); + break; + + case MusicService.RELEASE_WAKELOCK: + service.releaseWakeLock(); + break; + + case MusicService.PLAY_SONG: + service.playSongAtImpl(msg.arg1); + break; + + case MusicService.SET_POSITION: + service.openTrackAndPrepareNextAt(msg.arg1); + service.notifyChange(PLAY_STATE_CHANGED); + break; + + case MusicService.PREPARE_NEXT: + service.prepareNextImpl(); + break; + + case MusicService.RESTORE_QUEUES: + service.restoreQueuesAndPositionIfNecessary(); + break; + + case MusicService.FOCUS_CHANGE: + switch (msg.arg1) { + case AudioManager.AUDIOFOCUS_GAIN: + if (!service.isPlaying() && service.isPausedByTransientLossOfFocus()) { + service.play(); + service.setPausedByTransientLossOfFocus(false); + } + removeMessages(DUCK); + sendEmptyMessage(MusicService.UNDUCK); + break; + + case AudioManager.AUDIOFOCUS_LOSS: + // Lost focus for an unbounded amount of time: stop playback and release media playback + service.pause(); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + // Lost focus for a short time, but we have to stop + // playback. We don't release the media playback because playback + // is likely to resume + boolean wasPlaying = service.isPlaying(); + service.pause(); + service.setPausedByTransientLossOfFocus(wasPlaying); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + // Lost focus for a short time, but it's ok to keep playing + // at an attenuated level + removeMessages(MusicService.UNDUCK); + sendEmptyMessage(DUCK); + break; + } + break; } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/ArtistSignatureUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/ArtistSignatureUtil.java index fece1ea59..6418dae5b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/ArtistSignatureUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/ArtistSignatureUtil.java @@ -17,42 +17,38 @@ package code.name.monkey.retromusic.util; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; - import androidx.annotation.NonNull; - import com.bumptech.glide.signature.StringSignature; -/** - * @author Karim Abou Zeid (kabouzeid) - */ +/** @author Karim Abou Zeid (kabouzeid) */ public class ArtistSignatureUtil { - private static final String ARTIST_SIGNATURE_PREFS = "artist_signatures"; + private static final String ARTIST_SIGNATURE_PREFS = "artist_signatures"; - private static ArtistSignatureUtil sInstance; + private static ArtistSignatureUtil sInstance; - private final SharedPreferences mPreferences; + private final SharedPreferences mPreferences; - private ArtistSignatureUtil(@NonNull final Context context) { - mPreferences = context.getSharedPreferences(ARTIST_SIGNATURE_PREFS, Context.MODE_PRIVATE); + private ArtistSignatureUtil(@NonNull final Context context) { + mPreferences = context.getSharedPreferences(ARTIST_SIGNATURE_PREFS, Context.MODE_PRIVATE); + } + + public static ArtistSignatureUtil getInstance(@NonNull final Context context) { + if (sInstance == null) { + sInstance = new ArtistSignatureUtil(context.getApplicationContext()); } + return sInstance; + } - public static ArtistSignatureUtil getInstance(@NonNull final Context context) { - if (sInstance == null) { - sInstance = new ArtistSignatureUtil(context.getApplicationContext()); - } - return sInstance; - } + @SuppressLint("CommitPrefEdits") + public void updateArtistSignature(String artistName) { + mPreferences.edit().putLong(artistName, System.currentTimeMillis()).commit(); + } - @SuppressLint("CommitPrefEdits") - public void updateArtistSignature(String artistName) { - mPreferences.edit().putLong(artistName, System.currentTimeMillis()).commit(); - } + public long getArtistSignatureRaw(String artistName) { + return mPreferences.getLong(artistName, 0); + } - public long getArtistSignatureRaw(String artistName) { - return mPreferences.getLong(artistName, 0); - } - - public StringSignature getArtistSignature(String artistName) { - return new StringSignature(String.valueOf(getArtistSignatureRaw(artistName))); - } + public StringSignature getArtistSignature(String artistName) { + return new StringSignature(String.valueOf(getArtistSignatureRaw(artistName))); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java b/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java index b0ee4cf57..026c37199 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java @@ -8,182 +8,181 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; - import androidx.annotation.NonNull; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.model.Song; import com.bumptech.glide.Glide; - import java.util.ArrayList; import java.util.List; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.model.Song; - - public class AutoGeneratedPlaylistBitmap { - private static final String TAG = "AutoGeneratedPB"; + private static final String TAG = "AutoGeneratedPB"; + /* + public static Bitmap getBitmapWithCollectionFrame(Context context, List songPlaylist, boolean round, boolean blur) { + Bitmap bitmap = getBitmap(context,songPlaylist,round,blur); + int w = bitmap.getWidth(); + Bitmap ret = Bitmap.createBitmap(w,w,Bitmap.Config.ARGB_8888); + } + */ + public static Bitmap getBitmap( + Context context, List songPlaylist, boolean round, boolean blur) { + if (songPlaylist == null) return null; + long start = System.currentTimeMillis(); + // lấy toàn bộ album id, loại bỏ trùng nhau + List albumID = new ArrayList<>(); + for (Song song : songPlaylist) { + if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId()); + } + + long start2 = System.currentTimeMillis() - start; + + // lấy toàn bộ art tồn tại + List art = new ArrayList(); + for (Long id : albumID) { + Bitmap bitmap = getBitmapWithAlbumId(context, id); + if (bitmap != null) art.add(bitmap); + if (art.size() == 6) break; + } + return MergedImageUtils.INSTANCE.joinImages(art); /* - public static Bitmap getBitmapWithCollectionFrame(Context context, List songPlaylist, boolean round, boolean blur) { - Bitmap bitmap = getBitmap(context,songPlaylist,round,blur); - int w = bitmap.getWidth(); - Bitmap ret = Bitmap.createBitmap(w,w,Bitmap.Config.ARGB_8888); + + long start3 = System.currentTimeMillis() - start2 - start; + Bitmap ret; + switch (art.size()) { + // lấy hình mặc định + case 0: + ret = getDefaultBitmap(context, round).copy(Bitmap.Config.ARGB_8888, false); + break; + // dùng hình duy nhất + case 1: + if (round) + ret = BitmapEditor.getRoundedCornerBitmap(art.get(0), art.get(0).getWidth() / 40); + else ret = art.get(0); + break; + // từ 2 trở lên ta cần vẽ canvas + default: + ret = getBitmapCollection(art, round); } - */ - public static Bitmap getBitmap(Context context, List songPlaylist, boolean round, boolean blur) { - if (songPlaylist == null) return null; - long start = System.currentTimeMillis(); - // lấy toàn bộ album id, loại bỏ trùng nhau - List albumID = new ArrayList<>(); - for (Song song : songPlaylist) { - if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId()); - } + int w = ret.getWidth(); + if (blur) + return BitmapEditor.GetRoundedBitmapWithBlurShadow(context, ret, w / 24, w / 24, w / 24, w / 24, 0, 200, w / 40, 1); - long start2 = System.currentTimeMillis() - start; + Log.d(TAG, "getBitmap: time = " + (System.currentTimeMillis() - start) + ", start2 = " + start2 + ", start3 = " + start3); + return ret;*/ + } - // lấy toàn bộ art tồn tại - List art = new ArrayList(); - for (Long id : albumID) { - Bitmap bitmap = getBitmapWithAlbumId(context, id); - if (bitmap != null) art.add(bitmap); - if (art.size() == 6) break; - } - return MergedImageUtils.INSTANCE.joinImages(art); - /* + private static Bitmap getBitmapCollection(ArrayList art, boolean round) { + long start = System.currentTimeMillis(); + // lấy kích thước là kích thước của bitmap lớn nhất + int max_width = art.get(0).getWidth(); + for (Bitmap b : art) if (max_width < b.getWidth()) max_width = b.getWidth(); + Bitmap bitmap = Bitmap.createBitmap(max_width, max_width, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(false); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(max_width / 100); + paint.setColor(0xffffffff); + switch (art.size()) { + case 2: + canvas.drawBitmap(art.get(1), null, new Rect(0, 0, max_width, max_width), null); + canvas.drawBitmap( + art.get(0), null, new Rect(-max_width / 2, 0, max_width / 2, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + break; + case 3: + canvas.drawBitmap( + art.get(0), null, new Rect(-max_width / 4, 0, 3 * max_width / 4, max_width), null); + canvas.drawBitmap( + art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); + canvas.drawBitmap( + art.get(2), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + canvas.drawLine(max_width / 2, max_width / 2, max_width, max_width / 2, paint); + break; + case 4: + canvas.drawBitmap(art.get(0), null, new Rect(0, 0, max_width / 2, max_width / 2), null); + canvas.drawBitmap( + art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); + canvas.drawBitmap( + art.get(2), null, new Rect(0, max_width / 2, max_width / 2, max_width), null); + canvas.drawBitmap( + art.get(3), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + canvas.drawLine(0, max_width / 2, max_width, max_width / 2, paint); + break; + // default: canvas.drawBitmap(art.get(0),null,new Rect(0,0,max_width,max_width),null); + default: - long start3 = System.currentTimeMillis() - start2 - start; - Bitmap ret; - switch (art.size()) { - // lấy hình mặc định + // độ rộng của des bitmap + float w = (float) (Math.sqrt(2) / 2 * max_width); + float b = (float) (max_width / Math.sqrt(5)); + // khoảng cách định nghĩa, dùng để tính vị trí tâm của 4 bức hình xung quanh + float d = (float) (max_width * (0.5f - 1 / Math.sqrt(10))); + float deg = 45; + + for (int i = 0; i < 5; i++) { + canvas.save(); + switch (i) { case 0: - ret = getDefaultBitmap(context, round).copy(Bitmap.Config.ARGB_8888, false); - break; - // dùng hình duy nhất + canvas.translate(max_width / 2, max_width / 2); + canvas.rotate(deg); + // b = (float) (max_width*Math.sqrt(2/5f)); + canvas.drawBitmap(art.get(0), null, new RectF(-b / 2, -b / 2, b / 2, b / 2), null); + break; case 1: - if (round) - ret = BitmapEditor.getRoundedCornerBitmap(art.get(0), art.get(0).getWidth() / 40); - else ret = art.get(0); - break; - // từ 2 trở lên ta cần vẽ canvas - default: - ret = getBitmapCollection(art, round); - } - int w = ret.getWidth(); - if (blur) - return BitmapEditor.GetRoundedBitmapWithBlurShadow(context, ret, w / 24, w / 24, w / 24, w / 24, 0, 200, w / 40, 1); - - Log.d(TAG, "getBitmap: time = " + (System.currentTimeMillis() - start) + ", start2 = " + start2 + ", start3 = " + start3); - return ret;*/ - } - - private static Bitmap getBitmapCollection(ArrayList art, boolean round) { - long start = System.currentTimeMillis(); - // lấy kích thước là kích thước của bitmap lớn nhất - int max_width = art.get(0).getWidth(); - for (Bitmap b : art) if (max_width < b.getWidth()) max_width = b.getWidth(); - Bitmap bitmap = Bitmap.createBitmap(max_width, max_width, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setAntiAlias(false); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(max_width / 100); - paint.setColor(0xffffffff); - switch (art.size()) { + canvas.translate(d, 0); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(w / 2, -w / 2, w / 2, w / 2, paint); + break; case 2: - canvas.drawBitmap(art.get(1), null, new Rect(0, 0, max_width, max_width), null); - canvas.drawBitmap(art.get(0), null, new Rect(-max_width / 2, 0, max_width / 2, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - break; + canvas.translate(max_width, d); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, w / 2, w / 2, w / 2, paint); + break; case 3: - canvas.drawBitmap(art.get(0), null, new Rect(-max_width / 4, 0, 3 * max_width / 4, max_width), null); - canvas.drawBitmap(art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); - canvas.drawBitmap(art.get(2), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - canvas.drawLine(max_width / 2, max_width / 2, max_width, max_width / 2, paint); - break; + canvas.translate(max_width - d, max_width); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, -w / 2, -w / 2, w / 2, paint); + break; case 4: - canvas.drawBitmap(art.get(0), null, new Rect(0, 0, max_width / 2, max_width / 2), null); - canvas.drawBitmap(art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); - canvas.drawBitmap(art.get(2), null, new Rect(0, max_width / 2, max_width / 2, max_width), null); - canvas.drawBitmap(art.get(3), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - canvas.drawLine(0, max_width / 2, max_width, max_width / 2, paint); - break; - // default: canvas.drawBitmap(art.get(0),null,new Rect(0,0,max_width,max_width),null); - default: - - // độ rộng của des bitmap - float w = (float) (Math.sqrt(2) / 2 * max_width); - float b = (float) (max_width / Math.sqrt(5)); - // khoảng cách định nghĩa, dùng để tính vị trí tâm của 4 bức hình xung quanh - float d = (float) (max_width * (0.5f - 1 / Math.sqrt(10))); - float deg = 45; - - for (int i = 0; i < 5; i++) { - canvas.save(); - switch (i) { - case 0: - canvas.translate(max_width / 2, max_width / 2); - canvas.rotate(deg); - // b = (float) (max_width*Math.sqrt(2/5f)); - canvas.drawBitmap(art.get(0), null, new RectF(-b / 2, -b / 2, b / 2, b / 2), null); - break; - case 1: - canvas.translate(d, 0); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(w / 2, -w / 2, w / 2, w / 2, paint); - break; - case 2: - canvas.translate(max_width, d); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, w / 2, w / 2, w / 2, paint); - break; - case 3: - canvas.translate(max_width - d, max_width); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, -w / 2, -w / 2, w / 2, paint); - break; - case 4: - canvas.translate(0, max_width - d); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, -w / 2, w / 2, -w / 2, paint); - break; - } - canvas.restore(); - } - - - } - Log.d(TAG, "getBitmapCollection: smalltime = " + (System.currentTimeMillis() - start)); - if (round) - return BitmapEditor.getRoundedCornerBitmap(bitmap, bitmap.getWidth() / 40); - else return bitmap; - } - - private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Long id) { - try { - return Glide.with(context) - .load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id)) - .asBitmap() - .into(200, 200) - .get(); - } catch (Exception e) { - return null; + canvas.translate(0, max_width - d); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, -w / 2, w / 2, -w / 2, paint); + break; + } + canvas.restore(); } } + Log.d(TAG, "getBitmapCollection: smalltime = " + (System.currentTimeMillis() - start)); + if (round) return BitmapEditor.getRoundedCornerBitmap(bitmap, bitmap.getWidth() / 40); + else return bitmap; + } - public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) { - if (round) - return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); - return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); + private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Long id) { + try { + return Glide.with(context) + .load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id)) + .asBitmap() + .into(200, 200) + .get(); + } catch (Exception e) { + return null; } + } -} \ No newline at end of file + public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) { + if (round) + return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); + return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/BitmapEditor.java b/app/src/main/java/code/name/monkey/retromusic/util/BitmapEditor.java index 0f7961414..fac5bae22 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/BitmapEditor.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/BitmapEditor.java @@ -25,944 +25,975 @@ import android.renderscript.ScriptIntrinsicBlur; import android.view.View; import android.widget.ImageView; -/** - * Created by trung on 7/11/2017. - */ - +/** Created by trung on 7/11/2017. */ public final class BitmapEditor { - /** - * Stack Blur v1.0 from - * http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - * Java Author: Mario Klingemann - * http://incubator.quasimondo.com - *

- * created Feburary 29, 2004 - * Android port : Yahel Bouaziz - * http://www.kayenko.com - * ported april 5th, 2012 - *

- * This is A compromise between Gaussian Blur and Box blur - * It creates much better looking blurs than Box Blur, but is - * 7x faster than my Gaussian Blur implementation. - *

- * I called it Stack Blur because this describes best how this - * filter works internally: it creates A kind of moving stack - * of colors whilst scanning through the image. Thereby it - * just has to add one new block of color to the right side - * of the stack and removeFromParent the leftmost color. The remaining - * colors on the topmost layer of the stack are either added on - * or reduced by one, depending on if they are on the right or - * on the x side of the stack. - *

- * If you are using this algorithm in your code please add - * the following line: - * Stack Blur Algorithm by Mario Klingemann - */ + /** + * Stack Blur v1.0 from http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html Java + * Author: Mario Klingemann http://incubator.quasimondo.com + * + *

created Feburary 29, 2004 Android port : Yahel Bouaziz + * http://www.kayenko.com ported april 5th, 2012 + * + *

This is A compromise between Gaussian Blur and Box blur It creates much better looking blurs + * than Box Blur, but is 7x faster than my Gaussian Blur implementation. + * + *

I called it Stack Blur because this describes best how this filter works internally: it + * creates A kind of moving stack of colors whilst scanning through the image. Thereby it just has + * to add one new block of color to the right side of the stack and removeFromParent the leftmost + * color. The remaining colors on the topmost layer of the stack are either added on or reduced by + * one, depending on if they are on the right or on the x side of the stack. + * + *

If you are using this algorithm in your code please add the following line: Stack Blur + * Algorithm by Mario Klingemann + */ + public static Bitmap FastBlurSupportAlpha(Bitmap sentBitmap, float scale, int radius) { - public static Bitmap FastBlurSupportAlpha(Bitmap sentBitmap, float scale, int radius) { + int width = Math.round(sentBitmap.getWidth() * scale); + int height = Math.round(sentBitmap.getHeight() * scale); + sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); - int width = Math.round(sentBitmap.getWidth() * scale); - int height = Math.round(sentBitmap.getHeight() * scale); - sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); + Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); - Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); - - if (radius < 1) { - return (null); - } - - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - - int[] pix = new int[w * h]; - // Log.e("pix", w + " " + h + " " + pix.length); - bitmap.getPixels(pix, 0, w, 0, 0, w, h); - - int wm = w - 1; - int hm = h - 1; - int wh = w * h; - int div = radius + radius + 1; - - int[] r = new int[wh]; - int[] g = new int[wh]; - int[] b = new int[wh]; - int[] a = new int[wh]; - int rsum, gsum, bsum, asum, x, y, i, p, yp, yi, yw; - int[] vmin = new int[Math.max(w, h)]; - - int divsum = (div + 1) >> 1; - divsum *= divsum; - int[] dv = new int[256 * divsum]; - for (i = 0; i < 256 * divsum; i++) { - dv[i] = (i / divsum); - } - - yw = yi = 0; - - int[][] stack = new int[div][4]; - int stackpointer; - int stackstart; - int[] sir; - int rbs; - int r1 = radius + 1; - int routsum, goutsum, boutsum, aoutsum; - int rinsum, ginsum, binsum, ainsum; - - for (y = 0; y < h; y++) { - rinsum = ginsum = binsum = ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0; - for (i = -radius; i <= radius; i++) { - p = pix[yi + Math.min(wm, Math.max(i, 0))]; - sir = stack[i + radius]; - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - sir[3] = 0xff & (p >> 24); - - rbs = r1 - Math.abs(i); - rsum += sir[0] * rbs; - gsum += sir[1] * rbs; - bsum += sir[2] * rbs; - asum += sir[3] * rbs; - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - ainsum += sir[3]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - aoutsum += sir[3]; - } - } - stackpointer = radius; - - for (x = 0; x < w; x++) { - - r[yi] = dv[rsum]; - g[yi] = dv[gsum]; - b[yi] = dv[bsum]; - a[yi] = dv[asum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - asum -= aoutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - aoutsum -= sir[3]; - - if (y == 0) { - vmin[x] = Math.min(x + radius + 1, wm); - } - p = pix[yw + vmin[x]]; - - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - sir[3] = 0xff & (p >> 24); - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - ainsum += sir[3]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - asum += ainsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[(stackpointer) % div]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - aoutsum += sir[3]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - ainsum -= sir[3]; - - yi++; - } - yw += w; - } - for (x = 0; x < w; x++) { - rinsum = ginsum = binsum = ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0; - yp = -radius * w; - for (i = -radius; i <= radius; i++) { - yi = Math.max(0, yp) + x; - - sir = stack[i + radius]; - - sir[0] = r[yi]; - sir[1] = g[yi]; - sir[2] = b[yi]; - sir[3] = a[yi]; - - rbs = r1 - Math.abs(i); - - rsum += r[yi] * rbs; - gsum += g[yi] * rbs; - bsum += b[yi] * rbs; - asum += a[yi] * rbs; - - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - ainsum += sir[3]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - aoutsum += sir[3]; - } - - if (i < hm) { - yp += w; - } - } - yi = x; - stackpointer = radius; - for (y = 0; y < h; y++) { - pix[yi] = (dv[asum] << 24) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - asum -= aoutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - aoutsum -= sir[3]; - - if (x == 0) { - vmin[y] = Math.min(y + r1, hm) * w; - } - p = x + vmin[y]; - - - sir[0] = r[p]; - sir[1] = g[p]; - sir[2] = b[p]; - sir[3] = a[p]; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - ainsum += sir[3]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - asum += ainsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - aoutsum += sir[3]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - ainsum -= sir[3]; - - yi += w; - } - } - - // Log.e("pix", w + " " + h + " " + pix.length); - bitmap.setPixels(pix, 0, w, 0, 0, w, h); - - return (bitmap); + if (radius < 1) { + return (null); } - public static boolean PerceivedBrightness(int will_White, int[] c) { - double TBT = Math.sqrt(c[0] * c[0] * .241 + c[1] * c[1] * .691 + c[2] * c[2] * .068); - // Log.d("themee",TBT+""); - return !(TBT > will_White); + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + int[] pix = new int[w * h]; + // Log.e("pix", w + " " + h + " " + pix.length); + bitmap.getPixels(pix, 0, w, 0, 0, w, h); + + int wm = w - 1; + int hm = h - 1; + int wh = w * h; + int div = radius + radius + 1; + + int[] r = new int[wh]; + int[] g = new int[wh]; + int[] b = new int[wh]; + int[] a = new int[wh]; + int rsum, gsum, bsum, asum, x, y, i, p, yp, yi, yw; + int[] vmin = new int[Math.max(w, h)]; + + int divsum = (div + 1) >> 1; + divsum *= divsum; + int[] dv = new int[256 * divsum]; + for (i = 0; i < 256 * divsum; i++) { + dv[i] = (i / divsum); } - public static int[] getAverageColorRGB(Bitmap bitmap) { - final int width = bitmap.getWidth(); - final int height = bitmap.getHeight(); - int size = width * height; - int pixelColor; - int r, g, b; - r = g = b = 0; - for (int x = 0; x < width; ++x) { - for (int y = 0; y < height; ++y) { - pixelColor = bitmap.getPixel(x, y); - if (pixelColor == 0) { - size--; - continue; - } - r += Color.red(pixelColor); - g += Color.green(pixelColor); - b += Color.blue(pixelColor); - } - } - r /= size; - g /= size; - b /= size; - return new int[]{ - r, g, b - }; - } + yw = yi = 0; - public static Bitmap updateSat(Bitmap src, float settingSat) { + int[][] stack = new int[div][4]; + int stackpointer; + int stackstart; + int[] sir; + int rbs; + int r1 = radius + 1; + int routsum, goutsum, boutsum, aoutsum; + int rinsum, ginsum, binsum, ainsum; - int w = src.getWidth(); - int h = src.getHeight(); + for (y = 0; y < h; y++) { + rinsum = + ginsum = + binsum = + ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0; + for (i = -radius; i <= radius; i++) { + p = pix[yi + Math.min(wm, Math.max(i, 0))]; + sir = stack[i + radius]; + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + sir[3] = 0xff & (p >> 24); - Bitmap bitmapResult = - Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - Canvas canvasResult = new Canvas(bitmapResult); - Paint paint = new Paint(); - ColorMatrix colorMatrix = new ColorMatrix(); - colorMatrix.setSaturation(settingSat); - ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix); - paint.setColorFilter(filter); - canvasResult.drawBitmap(src, 0, 0, paint); - canvasResult.setBitmap(null); - canvasResult = null; - return bitmapResult; - } - - /** - * Stack Blur v1.0 from - * http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - * Java Author: Mario Klingemann - * http://incubator.quasimondo.com - *

- * created Feburary 29, 2004 - * Android port : Yahel Bouaziz - * http://www.kayenko.com - * ported april 5th, 2012 - *

- * This is A compromise between Gaussian Blur and Box blur - * It creates much better looking blurs than Box Blur, but is - * 7x faster than my Gaussian Blur implementation. - *

- * I called it Stack Blur because this describes best how this - * filter works internally: it creates A kind of moving stack - * of colors whilst scanning through the image. Thereby it - * just has to add one new block of color to the right side - * of the stack and removeFromParent the leftmost color. The remaining - * colors on the topmost layer of the stack are either added on - * or reduced by one, depending on if they are on the right or - * on the x side of the stack. - *

- * If you are using this algorithm in your code please add - * the following line: - * Stack Blur Algorithm by Mario Klingemann - */ - - public static Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) { - - Bitmap afterscaleSentBitmap; - Bitmap bitmap; - if (scale != 1) { - int width = Math.round(sentBitmap.getWidth() * scale); //lấy chiều rộng làm tròn - int height = Math.round(sentBitmap.getHeight() * scale); // lấy chiều cao làm tròn - afterscaleSentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); // tạo bitmap scaled - bitmap = afterscaleSentBitmap.copy(afterscaleSentBitmap.getConfig(), true); - afterscaleSentBitmap.recycle(); + rbs = r1 - Math.abs(i); + rsum += sir[0] * rbs; + gsum += sir[1] * rbs; + bsum += sir[2] * rbs; + asum += sir[3] * rbs; + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + ainsum += sir[3]; } else { - bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); // đơn giản chỉ copy + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + aoutsum += sir[3]; } + } + stackpointer = radius; + for (x = 0; x < w; x++) { - if (radius < 1) { - return (sentBitmap.copy(sentBitmap.getConfig(), true)); + r[yi] = dv[rsum]; + g[yi] = dv[gsum]; + b[yi] = dv[bsum]; + a[yi] = dv[asum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + asum -= aoutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + aoutsum -= sir[3]; + + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm); } + p = pix[yw + vmin[x]]; - int w = bitmap.getWidth(); // w is the width of sample bitmap - int h = bitmap.getHeight(); // h is the height of sample bitmap + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + sir[3] = 0xff & (p >> 24); - int[] pix = new int[w * h]; // pix is the arrary of all bitmap pixel + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + ainsum += sir[3]; - bitmap.getPixels(pix, 0, w, 0, 0, w, h); + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + asum += ainsum; - int wm = w - 1; - int hm = h - 1; - int wh = w * h; - int div = radius + radius + 1; + stackpointer = (stackpointer + 1) % div; + sir = stack[(stackpointer) % div]; - int[] r = new int[wh]; - int[] g = new int[wh]; - int[] b = new int[wh]; - int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; - int[] vmin = new int[Math.max(w, h)]; + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + aoutsum += sir[3]; - int divsum = (div + 1) >> 1; - divsum *= divsum; - int[] dv = new int[256 * divsum]; - for (i = 0; i < 256 * divsum; i++) { - dv[i] = (i / divsum); - } + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + ainsum -= sir[3]; - yw = yi = 0; - - int[][] stack = new int[div][3]; - int stackpointer; - int stackstart; - int[] sir; - int rbs; - int r1 = radius + 1; - int routsum, goutsum, boutsum; - int rinsum, ginsum, binsum; - - for (y = 0; y < h; y++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - for (i = -radius; i <= radius; i++) { - p = pix[yi + Math.min(wm, Math.max(i, 0))]; - sir = stack[i + radius]; - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - rbs = r1 - Math.abs(i); - rsum += sir[0] * rbs; - gsum += sir[1] * rbs; - bsum += sir[2] * rbs; - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - } - stackpointer = radius; - - for (x = 0; x < w; x++) { - - r[yi] = dv[rsum]; - g[yi] = dv[gsum]; - b[yi] = dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (y == 0) { - vmin[x] = Math.min(x + radius + 1, wm); - } - p = pix[yw + vmin[x]]; - - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[(stackpointer) % div]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi++; - } - yw += w; - } - for (x = 0; x < w; x++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - yp = -radius * w; - for (i = -radius; i <= radius; i++) { - yi = Math.max(0, yp) + x; - - sir = stack[i + radius]; - - sir[0] = r[yi]; - sir[1] = g[yi]; - sir[2] = b[yi]; - - rbs = r1 - Math.abs(i); - - rsum += r[yi] * rbs; - gsum += g[yi] * rbs; - bsum += b[yi] * rbs; - - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - - if (i < hm) { - yp += w; - } - } - yi = x; - stackpointer = radius; - for (y = 0; y < h; y++) { - // Preserve alpha channel: ( 0xff000000 & pix[yi] ) - pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (x == 0) { - vmin[y] = Math.min(y + r1, hm) * w; - } - p = x + vmin[y]; - - sir[0] = r[p]; - sir[1] = g[p]; - sir[2] = b[p]; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi += w; - } - } - - - bitmap.setPixels(pix, 0, w, 0, 0, w, h); - - return (bitmap); + yi++; + } + yw += w; } + for (x = 0; x < w; x++) { + rinsum = + ginsum = + binsum = + ainsum = routsum = goutsum = boutsum = aoutsum = rsum = gsum = bsum = asum = 0; + yp = -radius * w; + for (i = -radius; i <= radius; i++) { + yi = Math.max(0, yp) + x; - public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int pixels) { - Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap - .getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); + sir = stack[i + radius]; - final int color = 0xff424242; - final Paint paint = new Paint(); - final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); - // final ScreenSize rectF = new ScreenSize(rect); - final float roundPx = pixels; + sir[0] = r[yi]; + sir[1] = g[yi]; + sir[2] = b[yi]; + sir[3] = a[yi]; - paint.setAntiAlias(true); - canvas.drawARGB(0, 0, 0, 0); - paint.setColor(color); - // canvas.drawRoundRect(rectF, roundPx, roundPx, paint); - canvas.drawPath(BitmapEditor.RoundedRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), roundPx, roundPx, false), paint); - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(bitmap, rect, rect, paint); + rbs = r1 - Math.abs(i); - return output; - } + rsum += r[yi] * rbs; + gsum += g[yi] * rbs; + bsum += b[yi] * rbs; + asum += a[yi] * rbs; - /** - * getResizedBitmap method is used to Resized the Image according to custom width and height - * - * @param image - * @param newHeight (new desired height) - * @param newWidth (new desired Width) - * @return image (new resized image) - */ - public static Bitmap getResizedBitmap(Bitmap image, int newHeight, int newWidth) { - int width = image.getWidth(); - int height = image.getHeight(); - float scaleWidth = ((float) newWidth) / width; - float scaleHeight = ((float) newHeight) / height; - // create A matrix for the manipulation - Matrix matrix = new Matrix(); - // onTap the bit map - matrix.postScale(scaleWidth, scaleHeight); - // recreate the new Bitmap - Bitmap resizedBitmap = Bitmap.createBitmap(image, 0, 0, width, height, - matrix, false); - return resizedBitmap; - } - - public static boolean TrueIfBitmapBigger(Bitmap bitmap, int size) { - int sizeBitmap = (bitmap.getHeight() > bitmap.getWidth()) ? bitmap.getHeight() : bitmap.getWidth(); - return sizeBitmap > size; - } - - public static Bitmap GetRoundedBitmapWithBlurShadow(Bitmap original, int paddingTop, int paddingBottom, int paddingLeft, int paddingRight) { - int original_width = original.getWidth(); - int orginal_height = original.getHeight(); - int bitmap_width = original_width + paddingLeft + paddingRight; - int bitmap_height = orginal_height + paddingTop + paddingBottom; - Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setStyle(Paint.Style.FILL); - //paint.setAlpha(60); - // canvas.drawRect(0,0,bitmap_width,bitmap_height,paint); - paint.setAntiAlias(true); - canvas.drawBitmap(original, paddingLeft, paddingTop, paint); - Bitmap blurred_bitmap = getBlurredWithGoodPerformance(bitmap, 1, 6, 4); - canvas.setBitmap(null); - bitmap.recycle(); - return blurred_bitmap; - } - - // Activity. - // | Original bitmap. - // | | To make the blur background, the original must to padding. - // | | | | | | - // V V V V V V - public static Bitmap GetRoundedBitmapWithBlurShadow(Context context, Bitmap original, int paddingTop, int paddingBottom, int paddingLeft, int paddingRight, - int TopBack // this value makes the overview bitmap is higher or belower the background. - , int alphaBlurBackground // this is the alpha of the background Bitmap, you need A number between 0 -> 255, the value recommend is 180. - , int valueBlurBackground // this is the value used to blur the background Bitmap, the recommended one is 12. - , int valueSaturationBlurBackground // this is the value used to background Bitmap more colorful, if valueBlur is 12, the valudeSaturation should be 2. - ) { - int original_width = original.getWidth(); - int orginal_height = original.getHeight(); - int bitmap_width = original_width + paddingLeft + paddingRight; - int bitmap_height = orginal_height + paddingTop + paddingBottom; - Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setStyle(Paint.Style.FILL); - paint.setAntiAlias(true); - canvas.drawBitmap(original, paddingLeft, paddingTop, paint); - Bitmap blurred_bitmap = getBlurredWithGoodPerformance(context, bitmap, 1, valueBlurBackground, valueSaturationBlurBackground); - // Bitmap blurred_bitmap= getBlurredWithGoodPerformance(context, bitmap,1,15,3); - Bitmap end_bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); - canvas.setBitmap(end_bitmap); - paint.setAlpha(alphaBlurBackground); - - canvas.drawBitmap(blurred_bitmap, new Rect(0, 0, blurred_bitmap.getWidth(), blurred_bitmap.getHeight()), new Rect(0, 0, bitmap_width, bitmap_height), paint); - paint.setAlpha(255); - - canvas.drawBitmap(bitmap, 0, TopBack, paint); // drawVisualWave cái lớn - canvas.setBitmap(null); - blurred_bitmap.recycle(); - bitmap.recycle(); - return end_bitmap; - } - - public static void setBitmapforImageView(ImageView imv, Bitmap apply) { - Bitmap old = ((BitmapDrawable) imv.getDrawable()).getBitmap(); - imv.setImageBitmap(apply); - if (old != null) - old.recycle(); - } - - public static Bitmap getBlurredWithGoodPerformance(Bitmap bitmap, int scale, int radius, int saturation) { - BitmapFactory.Options options = new BitmapFactory.Options(); - Bitmap bitmap1 = getResizedBitmap(bitmap, 50, 50); - Bitmap updateSatBitmap = updateSat(bitmap1, saturation); - Bitmap blurredBitmap = FastBlurSupportAlpha(updateSatBitmap, scale, radius); - - updateSatBitmap.recycle(); - bitmap1.recycle(); - return blurredBitmap; - } - - static public Path RoundedRect(float left, float top, float right, float bottom, float rx, float ry, boolean conformToOriginalPost) { - Path path = new Path(); - if (rx < 0) rx = 0; - if (ry < 0) ry = 0; - float width = right - left; - float height = bottom - top; - if (rx > width / 2) rx = width / 2; - if (ry > height / 2) ry = height / 2; - float widthMinusCorners = (width - (2 * rx)); // do dai phan "thang" cua chieu rong - float heightMinusCorners = (height - (2 * ry)); // do dai phan "thang" cua chieu dai - - path.moveTo(right, top + ry); // bat dau tu day - path.rQuadTo(0, -ry, -rx, -ry);//y-right corner - path.rLineTo(-widthMinusCorners, 0); - path.rQuadTo(-rx, 0, -rx, ry); //y-x corner - path.rLineTo(0, heightMinusCorners); - - if (conformToOriginalPost) { - path.rLineTo(0, ry); - path.rLineTo(width, 0); - path.rLineTo(0, -ry); + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + ainsum += sir[3]; } else { - - path.rQuadTo(0, ry, rx, ry);//bottom-x corner - path.rLineTo(widthMinusCorners, 0); - path.rQuadTo(rx, 0, rx, -ry); //bottom-right corner + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + aoutsum += sir[3]; } - path.rLineTo(0, -heightMinusCorners); - - path.close();//Given close, last lineto can be removed. - - return path; - } - - public static int mixTwoColors(int color1, int color2, float amount) { - final byte ALPHA_CHANNEL = 24; - final byte RED_CHANNEL = 16; - final byte GREEN_CHANNEL = 8; - final byte BLUE_CHANNEL = 0; - - final float inverseAmount = 1.0f - amount; - - int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + - ((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + - ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + - ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int b = ((int) (((float) (color1 & 0xff) * amount) + - ((float) (color2 & 0xff) * inverseAmount))) & 0xff; - - return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL; - } - - public static Bitmap getBlurredWithGoodPerformance(Context context, Bitmap bitmap, int scale, int radius, float saturation) { - Bitmap bitmap1 = getResizedBitmap(bitmap, 150, 150); - Bitmap updateSatBimap = updateSat(bitmap1, saturation); - Bitmap blurredBitmap = BlurBitmapWithRenderScript(context, updateSatBimap, radius); - updateSatBimap.recycle(); - bitmap1.recycle(); - return blurredBitmap; - } - - public static Bitmap getBlurredBimapWithRenderScript(Context context, Bitmap bitmapOriginal, float radius) { - //define this only once if blurring multiple times - RenderScript rs = RenderScript.create(context); - -//this will blur the bitmapOriginal with A radius of 8 and save it in bitmapOriginal - final Allocation input = Allocation.createFromBitmap(rs, bitmapOriginal); //use this constructor for best performance, because it uses USAGE_SHARED mode which reuses memory - final Allocation output = Allocation.createTyped(rs, input.getType()); - final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); - script.setRadius(radius); - script.setInput(input); - script.forEach(output); - output.copyTo(bitmapOriginal); - return bitmapOriginal; - } - - public static Bitmap BlurBitmapWithRenderScript(Context context, Bitmap bitmap, float radius) { - //Let's create an empty bitmap with the same size of the bitmap we want to blur - Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - - //Instantiate A new Renderscript - RenderScript rs = RenderScript.create(context); - - //Create an Intrinsic Blur Script using the Renderscript - ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); - - //Create the Allocations (in/out) with the Renderscript and the in/out bitmaps - Allocation allIn = Allocation.createFromBitmap(rs, bitmap); - Allocation allOut = Allocation.createFromBitmap(rs, outBitmap); - //Set the radius of the blur - blurScript.setRadius(radius); - - //Perform the Renderscript - blurScript.setInput(allIn); - blurScript.forEach(allOut); - - //Copy the final bitmap created by the out Allocation to the outBitmap - allOut.copyTo(outBitmap); - - //recycle the original bitmap - - //After finishing everything, we destroy the Renderscript. - rs.destroy(); - - return outBitmap; - - - } - - - public static Drawable covertBitmapToDrawable(Context context, Bitmap bitmap) { - Drawable d = new BitmapDrawable(context.getResources(), bitmap); - return d; - } - - public static Bitmap convertDrawableToBitmap(Drawable drawable) { - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); + if (i < hm) { + yp += w; } - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } + } + yi = x; + stackpointer = radius; + for (y = 0; y < h; y++) { + pix[yi] = (dv[asum] << 24) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - public static Bitmap changeBitmapColor(Bitmap sourceBitmap, int color) { - Bitmap resultBitmap = sourceBitmap.copy(sourceBitmap.getConfig(), true); - Paint paint = new Paint(); - ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); - paint.setColorFilter(filter); - Canvas canvas = new Canvas(resultBitmap); - canvas.drawBitmap(resultBitmap, 0, 0, paint); - return resultBitmap; - } + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + asum -= aoutsum; - /** - * @param mode - * @return 0 : CLEAR - *
1 : SRC - *
2 : DST - *
3 : SRC_OVER - *
4 : DST_OVER - *
5 : SRC_IN - *
6 : DST_IN - *
7 : SRC_OUT - *
8 : DST_OUT - *
9 : SRC_ATOP - *
10 : DST_ATOP - *
11 : XOR - *
12 : ADD - *
13 : MULTIPLY - *
14 : SCREEN - *
15 : OVERLAY - *
16 : DARKEN - *
17 : LIGHTEN - */ - public static PorterDuff.Mode getPorterMode(int mode) { - switch (mode) { - default: - case 0: - return PorterDuff.Mode.CLEAR; - case 1: - return PorterDuff.Mode.SRC; - case 2: - return PorterDuff.Mode.DST; - case 3: - return PorterDuff.Mode.SRC_OVER; - case 4: - return PorterDuff.Mode.DST_OVER; - case 5: - return PorterDuff.Mode.SRC_IN; - case 6: - return PorterDuff.Mode.DST_IN; - case 7: - return PorterDuff.Mode.SRC_OUT; - case 8: - return PorterDuff.Mode.DST_OUT; - case 9: - return PorterDuff.Mode.SRC_ATOP; - case 10: - return PorterDuff.Mode.DST_ATOP; - case 11: - return PorterDuff.Mode.XOR; - case 16: - return PorterDuff.Mode.DARKEN; - case 17: - return PorterDuff.Mode.LIGHTEN; - case 13: - return PorterDuff.Mode.MULTIPLY; - case 14: - return PorterDuff.Mode.SCREEN; - case 12: - return PorterDuff.Mode.ADD; - case 15: - return PorterDuff.Mode.OVERLAY; + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + aoutsum -= sir[3]; + + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w; } + p = x + vmin[y]; + + sir[0] = r[p]; + sir[1] = g[p]; + sir[2] = b[p]; + sir[3] = a[p]; + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + ainsum += sir[3]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + asum += ainsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[stackpointer]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + aoutsum += sir[3]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + ainsum -= sir[3]; + + yi += w; + } } - public static void applyNewColor4Bitmap(Context context, int[] idBitmaps, ImageView[] imageViews, int color, float alpha) { - android.content.res.Resources resource = context.getResources(); - int size = idBitmaps.length; - Bitmap usingBitmap, resultBitmap; - for (int i = 0; i < size; i++) { - usingBitmap = BitmapFactory.decodeResource(resource, idBitmaps[i]); - resultBitmap = changeBitmapColor(usingBitmap, color); - imageViews[i].setImageBitmap(resultBitmap); - imageViews[i].setAlpha(alpha); + // Log.e("pix", w + " " + h + " " + pix.length); + bitmap.setPixels(pix, 0, w, 0, 0, w, h); + + return (bitmap); + } + + public static boolean PerceivedBrightness(int will_White, int[] c) { + double TBT = Math.sqrt(c[0] * c[0] * .241 + c[1] * c[1] * .691 + c[2] * c[2] * .068); + // Log.d("themee",TBT+""); + return !(TBT > will_White); + } + + public static int[] getAverageColorRGB(Bitmap bitmap) { + final int width = bitmap.getWidth(); + final int height = bitmap.getHeight(); + int size = width * height; + int pixelColor; + int r, g, b; + r = g = b = 0; + for (int x = 0; x < width; ++x) { + for (int y = 0; y < height; ++y) { + pixelColor = bitmap.getPixel(x, y); + if (pixelColor == 0) { + size--; + continue; } + r += Color.red(pixelColor); + g += Color.green(pixelColor); + b += Color.blue(pixelColor); + } + } + r /= size; + g /= size; + b /= size; + return new int[] {r, g, b}; + } + + public static Bitmap updateSat(Bitmap src, float settingSat) { + + int w = src.getWidth(); + int h = src.getHeight(); + + Bitmap bitmapResult = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas canvasResult = new Canvas(bitmapResult); + Paint paint = new Paint(); + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(settingSat); + ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix); + paint.setColorFilter(filter); + canvasResult.drawBitmap(src, 0, 0, paint); + canvasResult.setBitmap(null); + canvasResult = null; + return bitmapResult; + } + + /** + * Stack Blur v1.0 from http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html Java + * Author: Mario Klingemann http://incubator.quasimondo.com + * + *

created Feburary 29, 2004 Android port : Yahel Bouaziz + * http://www.kayenko.com ported april 5th, 2012 + * + *

This is A compromise between Gaussian Blur and Box blur It creates much better looking blurs + * than Box Blur, but is 7x faster than my Gaussian Blur implementation. + * + *

I called it Stack Blur because this describes best how this filter works internally: it + * creates A kind of moving stack of colors whilst scanning through the image. Thereby it just has + * to add one new block of color to the right side of the stack and removeFromParent the leftmost + * color. The remaining colors on the topmost layer of the stack are either added on or reduced by + * one, depending on if they are on the right or on the x side of the stack. + * + *

If you are using this algorithm in your code please add the following line: Stack Blur + * Algorithm by Mario Klingemann + */ + public static Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) { + + Bitmap afterscaleSentBitmap; + Bitmap bitmap; + if (scale != 1) { + int width = Math.round(sentBitmap.getWidth() * scale); // lấy chiều rộng làm tròn + int height = Math.round(sentBitmap.getHeight() * scale); // lấy chiều cao làm tròn + afterscaleSentBitmap = + Bitmap.createScaledBitmap(sentBitmap, width, height, false); // tạo bitmap scaled + bitmap = afterscaleSentBitmap.copy(afterscaleSentBitmap.getConfig(), true); + afterscaleSentBitmap.recycle(); + } else { + bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); // đơn giản chỉ copy } - public static void applyNewColor4Bitmap(Context context, int idBitmap, ImageView applyView, int color, float alpha) { - - android.content.res.Resources resource = context.getResources(); - Bitmap usingBitmap = BitmapFactory.decodeResource(resource, idBitmap); - Bitmap resultBitmap = changeBitmapColor(usingBitmap, color); - applyView.setImageBitmap(resultBitmap); - applyView.setAlpha(alpha); - + if (radius < 1) { + return (sentBitmap.copy(sentBitmap.getConfig(), true)); } - public static Bitmap getBitmapFromView(View view) { - Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(bitmap); - view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); - view.draw(c); - return bitmap; + int w = bitmap.getWidth(); // w is the width of sample bitmap + int h = bitmap.getHeight(); // h is the height of sample bitmap + + int[] pix = new int[w * h]; // pix is the arrary of all bitmap pixel + + bitmap.getPixels(pix, 0, w, 0, 0, w, h); + + int wm = w - 1; + int hm = h - 1; + int wh = w * h; + int div = radius + radius + 1; + + int[] r = new int[wh]; + int[] g = new int[wh]; + int[] b = new int[wh]; + int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; + int[] vmin = new int[Math.max(w, h)]; + + int divsum = (div + 1) >> 1; + divsum *= divsum; + int[] dv = new int[256 * divsum]; + for (i = 0; i < 256 * divsum; i++) { + dv[i] = (i / divsum); } - public static Bitmap getBitmapFromView(View view, int left, int top, int right, int bottom) { - Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(bitmap); - view.layout(left, top, right, bottom); - view.draw(c); - return bitmap; + yw = yi = 0; + + int[][] stack = new int[div][3]; + int stackpointer; + int stackstart; + int[] sir; + int rbs; + int r1 = radius + 1; + int routsum, goutsum, boutsum; + int rinsum, ginsum, binsum; + + for (y = 0; y < h; y++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + for (i = -radius; i <= radius; i++) { + p = pix[yi + Math.min(wm, Math.max(i, 0))]; + sir = stack[i + radius]; + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + rbs = r1 - Math.abs(i); + rsum += sir[0] * rbs; + gsum += sir[1] * rbs; + bsum += sir[2] * rbs; + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + } + stackpointer = radius; + + for (x = 0; x < w; x++) { + + r[yi] = dv[rsum]; + g[yi] = dv[gsum]; + b[yi] = dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm); + } + p = pix[yw + vmin[x]]; + + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[(stackpointer) % div]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi++; + } + yw += w; + } + for (x = 0; x < w; x++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + yp = -radius * w; + for (i = -radius; i <= radius; i++) { + yi = Math.max(0, yp) + x; + + sir = stack[i + radius]; + + sir[0] = r[yi]; + sir[1] = g[yi]; + sir[2] = b[yi]; + + rbs = r1 - Math.abs(i); + + rsum += r[yi] * rbs; + gsum += g[yi] * rbs; + bsum += b[yi] * rbs; + + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + + if (i < hm) { + yp += w; + } + } + yi = x; + stackpointer = radius; + for (y = 0; y < h; y++) { + // Preserve alpha channel: ( 0xff000000 & pix[yi] ) + pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w; + } + p = x + vmin[y]; + + sir[0] = r[p]; + sir[1] = g[p]; + sir[2] = b[p]; + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[stackpointer]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi += w; + } } - public static Bitmap getBackgroundBitmapAViewWithParent(View childView, View parentView) { - int[] pos_child = new int[2]; - childView.getLocationOnScreen(pos_child); - return getBitmapFromView(parentView, pos_child[0], pos_child[1], parentView.getRight(), parentView.getBottom()); + bitmap.setPixels(pix, 0, w, 0, 0, w, h); + + return (bitmap); + } + + public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int pixels) { + Bitmap output = + Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + // final ScreenSize rectF = new ScreenSize(rect); + final float roundPx = pixels; + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + // canvas.drawRoundRect(rectF, roundPx, roundPx, paint); + canvas.drawPath( + BitmapEditor.RoundedRect( + 0, 0, bitmap.getWidth(), bitmap.getHeight(), roundPx, roundPx, false), + paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + + /** + * getResizedBitmap method is used to Resized the Image according to custom width and height + * + * @param image + * @param newHeight (new desired height) + * @param newWidth (new desired Width) + * @return image (new resized image) + */ + public static Bitmap getResizedBitmap(Bitmap image, int newHeight, int newWidth) { + int width = image.getWidth(); + int height = image.getHeight(); + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + // create A matrix for the manipulation + Matrix matrix = new Matrix(); + // onTap the bit map + matrix.postScale(scaleWidth, scaleHeight); + // recreate the new Bitmap + Bitmap resizedBitmap = Bitmap.createBitmap(image, 0, 0, width, height, matrix, false); + return resizedBitmap; + } + + public static boolean TrueIfBitmapBigger(Bitmap bitmap, int size) { + int sizeBitmap = + (bitmap.getHeight() > bitmap.getWidth()) ? bitmap.getHeight() : bitmap.getWidth(); + return sizeBitmap > size; + } + + public static Bitmap GetRoundedBitmapWithBlurShadow( + Bitmap original, int paddingTop, int paddingBottom, int paddingLeft, int paddingRight) { + int original_width = original.getWidth(); + int orginal_height = original.getHeight(); + int bitmap_width = original_width + paddingLeft + paddingRight; + int bitmap_height = orginal_height + paddingTop + paddingBottom; + Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + // paint.setAlpha(60); + // canvas.drawRect(0,0,bitmap_width,bitmap_height,paint); + paint.setAntiAlias(true); + canvas.drawBitmap(original, paddingLeft, paddingTop, paint); + Bitmap blurred_bitmap = getBlurredWithGoodPerformance(bitmap, 1, 6, 4); + canvas.setBitmap(null); + bitmap.recycle(); + return blurred_bitmap; + } + + // Activity. + // | + // Original bitmap. + // | + // | To make the blur background, the original must to padding. + // | + // | | | | + // | + // V + // V V V V + // V + public static Bitmap GetRoundedBitmapWithBlurShadow( + Context context, + Bitmap original, + int paddingTop, + int paddingBottom, + int paddingLeft, + int paddingRight, + int TopBack // this value makes the overview bitmap is higher or belower the background. + , + int alphaBlurBackground // this is the alpha of the background Bitmap, you need A number + // between 0 -> 255, the value recommend is 180. + , + int valueBlurBackground // this is the value used to blur the background Bitmap, the + // recommended one is 12. + , + int valueSaturationBlurBackground // this is the value used to background Bitmap more + // colorful, if valueBlur is 12, the valudeSaturation should + // be 2. + ) { + int original_width = original.getWidth(); + int orginal_height = original.getHeight(); + int bitmap_width = original_width + paddingLeft + paddingRight; + int bitmap_height = orginal_height + paddingTop + paddingBottom; + Bitmap bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + canvas.drawBitmap(original, paddingLeft, paddingTop, paint); + Bitmap blurred_bitmap = + getBlurredWithGoodPerformance( + context, bitmap, 1, valueBlurBackground, valueSaturationBlurBackground); + // Bitmap blurred_bitmap= getBlurredWithGoodPerformance(context, bitmap,1,15,3); + Bitmap end_bitmap = Bitmap.createBitmap(bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + canvas.setBitmap(end_bitmap); + paint.setAlpha(alphaBlurBackground); + + canvas.drawBitmap( + blurred_bitmap, + new Rect(0, 0, blurred_bitmap.getWidth(), blurred_bitmap.getHeight()), + new Rect(0, 0, bitmap_width, bitmap_height), + paint); + paint.setAlpha(255); + + canvas.drawBitmap(bitmap, 0, TopBack, paint); // drawVisualWave cái lớn + canvas.setBitmap(null); + blurred_bitmap.recycle(); + bitmap.recycle(); + return end_bitmap; + } + + public static void setBitmapforImageView(ImageView imv, Bitmap apply) { + Bitmap old = ((BitmapDrawable) imv.getDrawable()).getBitmap(); + imv.setImageBitmap(apply); + if (old != null) old.recycle(); + } + + public static Bitmap getBlurredWithGoodPerformance( + Bitmap bitmap, int scale, int radius, int saturation) { + BitmapFactory.Options options = new BitmapFactory.Options(); + Bitmap bitmap1 = getResizedBitmap(bitmap, 50, 50); + Bitmap updateSatBitmap = updateSat(bitmap1, saturation); + Bitmap blurredBitmap = FastBlurSupportAlpha(updateSatBitmap, scale, radius); + + updateSatBitmap.recycle(); + bitmap1.recycle(); + return blurredBitmap; + } + + public static Path RoundedRect( + float left, + float top, + float right, + float bottom, + float rx, + float ry, + boolean conformToOriginalPost) { + Path path = new Path(); + if (rx < 0) rx = 0; + if (ry < 0) ry = 0; + float width = right - left; + float height = bottom - top; + if (rx > width / 2) rx = width / 2; + if (ry > height / 2) ry = height / 2; + float widthMinusCorners = (width - (2 * rx)); // do dai phan "thang" cua chieu rong + float heightMinusCorners = (height - (2 * ry)); // do dai phan "thang" cua chieu dai + + path.moveTo(right, top + ry); // bat dau tu day + path.rQuadTo(0, -ry, -rx, -ry); // y-right corner + path.rLineTo(-widthMinusCorners, 0); + path.rQuadTo(-rx, 0, -rx, ry); // y-x corner + path.rLineTo(0, heightMinusCorners); + + if (conformToOriginalPost) { + path.rLineTo(0, ry); + path.rLineTo(width, 0); + path.rLineTo(0, -ry); + } else { + + path.rQuadTo(0, ry, rx, ry); // bottom-x corner + path.rLineTo(widthMinusCorners, 0); + path.rQuadTo(rx, 0, rx, -ry); // bottom-right corner } - public static Bitmap getBackgroundBlurAViewWithParent(Activity activity, View childView, View parentView) { - Bitmap b1 = getBackgroundBitmapAViewWithParent(childView, parentView); - Bitmap b2 = getBlurredWithGoodPerformance(activity, b1, 1, 8, 2); - b1.recycle(); - return b2; + path.rLineTo(0, -heightMinusCorners); + + path.close(); // Given close, last lineto can be removed. + + return path; + } + + public static int mixTwoColors(int color1, int color2, float amount) { + final byte ALPHA_CHANNEL = 24; + final byte RED_CHANNEL = 16; + final byte GREEN_CHANNEL = 8; + final byte BLUE_CHANNEL = 0; + + final float inverseAmount = 1.0f - amount; + + int a = + ((int) + (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + + ((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) + & 0xff; + int r = + ((int) + (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + + ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) + & 0xff; + int g = + ((int) + (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + + ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) + & 0xff; + int b = + ((int) (((float) (color1 & 0xff) * amount) + ((float) (color2 & 0xff) * inverseAmount))) + & 0xff; + + return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL; + } + + public static Bitmap getBlurredWithGoodPerformance( + Context context, Bitmap bitmap, int scale, int radius, float saturation) { + Bitmap bitmap1 = getResizedBitmap(bitmap, 150, 150); + Bitmap updateSatBimap = updateSat(bitmap1, saturation); + Bitmap blurredBitmap = BlurBitmapWithRenderScript(context, updateSatBimap, radius); + updateSatBimap.recycle(); + bitmap1.recycle(); + return blurredBitmap; + } + + public static Bitmap getBlurredBimapWithRenderScript( + Context context, Bitmap bitmapOriginal, float radius) { + // define this only once if blurring multiple times + RenderScript rs = RenderScript.create(context); + + // this will blur the bitmapOriginal with A radius of 8 and save it in bitmapOriginal + final Allocation input = + Allocation.createFromBitmap( + rs, bitmapOriginal); // use this constructor for best performance, because it uses + // USAGE_SHARED mode which reuses memory + final Allocation output = Allocation.createTyped(rs, input.getType()); + final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + script.setRadius(radius); + script.setInput(input); + script.forEach(output); + output.copyTo(bitmapOriginal); + return bitmapOriginal; + } + + public static Bitmap BlurBitmapWithRenderScript(Context context, Bitmap bitmap, float radius) { + // Let's create an empty bitmap with the same size of the bitmap we want to blur + Bitmap outBitmap = + Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + + // Instantiate A new Renderscript + RenderScript rs = RenderScript.create(context); + + // Create an Intrinsic Blur Script using the Renderscript + ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + + // Create the Allocations (in/out) with the Renderscript and the in/out bitmaps + Allocation allIn = Allocation.createFromBitmap(rs, bitmap); + Allocation allOut = Allocation.createFromBitmap(rs, outBitmap); + // Set the radius of the blur + blurScript.setRadius(radius); + + // Perform the Renderscript + blurScript.setInput(allIn); + blurScript.forEach(allOut); + + // Copy the final bitmap created by the out Allocation to the outBitmap + allOut.copyTo(outBitmap); + + // recycle the original bitmap + + // After finishing everything, we destroy the Renderscript. + rs.destroy(); + + return outBitmap; + } + + public static Drawable covertBitmapToDrawable(Context context, Bitmap bitmap) { + Drawable d = new BitmapDrawable(context.getResources(), bitmap); + return d; + } + + public static Bitmap convertDrawableToBitmap(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); } -} \ No newline at end of file + Bitmap bitmap = + Bitmap.createBitmap( + drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + public static Bitmap changeBitmapColor(Bitmap sourceBitmap, int color) { + Bitmap resultBitmap = sourceBitmap.copy(sourceBitmap.getConfig(), true); + Paint paint = new Paint(); + ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); + paint.setColorFilter(filter); + Canvas canvas = new Canvas(resultBitmap); + canvas.drawBitmap(resultBitmap, 0, 0, paint); + return resultBitmap; + } + + /** + * @param mode + * @return 0 : CLEAR
+ * 1 : SRC
+ * 2 : DST
+ * 3 : SRC_OVER
+ * 4 : DST_OVER
+ * 5 : SRC_IN
+ * 6 : DST_IN
+ * 7 : SRC_OUT
+ * 8 : DST_OUT
+ * 9 : SRC_ATOP
+ * 10 : DST_ATOP
+ * 11 : XOR
+ * 12 : ADD
+ * 13 : MULTIPLY
+ * 14 : SCREEN
+ * 15 : OVERLAY
+ * 16 : DARKEN
+ * 17 : LIGHTEN + */ + public static PorterDuff.Mode getPorterMode(int mode) { + switch (mode) { + default: + case 0: + return PorterDuff.Mode.CLEAR; + case 1: + return PorterDuff.Mode.SRC; + case 2: + return PorterDuff.Mode.DST; + case 3: + return PorterDuff.Mode.SRC_OVER; + case 4: + return PorterDuff.Mode.DST_OVER; + case 5: + return PorterDuff.Mode.SRC_IN; + case 6: + return PorterDuff.Mode.DST_IN; + case 7: + return PorterDuff.Mode.SRC_OUT; + case 8: + return PorterDuff.Mode.DST_OUT; + case 9: + return PorterDuff.Mode.SRC_ATOP; + case 10: + return PorterDuff.Mode.DST_ATOP; + case 11: + return PorterDuff.Mode.XOR; + case 16: + return PorterDuff.Mode.DARKEN; + case 17: + return PorterDuff.Mode.LIGHTEN; + case 13: + return PorterDuff.Mode.MULTIPLY; + case 14: + return PorterDuff.Mode.SCREEN; + case 12: + return PorterDuff.Mode.ADD; + case 15: + return PorterDuff.Mode.OVERLAY; + } + } + + public static void applyNewColor4Bitmap( + Context context, int[] idBitmaps, ImageView[] imageViews, int color, float alpha) { + android.content.res.Resources resource = context.getResources(); + int size = idBitmaps.length; + Bitmap usingBitmap, resultBitmap; + for (int i = 0; i < size; i++) { + usingBitmap = BitmapFactory.decodeResource(resource, idBitmaps[i]); + resultBitmap = changeBitmapColor(usingBitmap, color); + imageViews[i].setImageBitmap(resultBitmap); + imageViews[i].setAlpha(alpha); + } + } + + public static void applyNewColor4Bitmap( + Context context, int idBitmap, ImageView applyView, int color, float alpha) { + + android.content.res.Resources resource = context.getResources(); + Bitmap usingBitmap = BitmapFactory.decodeResource(resource, idBitmap); + Bitmap resultBitmap = changeBitmapColor(usingBitmap, color); + applyView.setImageBitmap(resultBitmap); + applyView.setAlpha(alpha); + } + + public static Bitmap getBitmapFromView(View view) { + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + view.draw(c); + return bitmap; + } + + public static Bitmap getBitmapFromView(View view, int left, int top, int right, int bottom) { + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + view.layout(left, top, right, bottom); + view.draw(c); + return bitmap; + } + + public static Bitmap getBackgroundBitmapAViewWithParent(View childView, View parentView) { + int[] pos_child = new int[2]; + childView.getLocationOnScreen(pos_child); + return getBitmapFromView( + parentView, pos_child[0], pos_child[1], parentView.getRight(), parentView.getBottom()); + } + + public static Bitmap getBackgroundBlurAViewWithParent( + Activity activity, View childView, View parentView) { + Bitmap b1 = getBackgroundBitmapAViewWithParent(childView, parentView); + Bitmap b2 = getBlurredWithGoodPerformance(activity, b1, 1, 8, 2); + b1.recycle(); + return b2; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/CalendarUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/CalendarUtil.java index e126a37c2..b396df9ab 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/CalendarUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/CalendarUtil.java @@ -17,126 +17,124 @@ package code.name.monkey.retromusic.util; import java.util.Calendar; import java.util.GregorianCalendar; -/** - * @author Eugene Cheung (arkon) - */ +/** @author Eugene Cheung (arkon) */ public class CalendarUtil { - private static final long MS_PER_MINUTE = 60 * 1000; - private static final long MS_PER_DAY = 24 * 60 * MS_PER_MINUTE; + private static final long MS_PER_MINUTE = 60 * 1000; + private static final long MS_PER_DAY = 24 * 60 * MS_PER_MINUTE; - private Calendar calendar; + private Calendar calendar; - public CalendarUtil() { - this.calendar = Calendar.getInstance(); + public CalendarUtil() { + this.calendar = Calendar.getInstance(); + } + + /** + * Returns the time elapsed so far today in milliseconds. + * + * @return Time elapsed today in milliseconds. + */ + public long getElapsedToday() { + // Time elapsed so far today + return (calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE)) * MS_PER_MINUTE + + calendar.get(Calendar.SECOND) * 1000 + + calendar.get(Calendar.MILLISECOND); + } + + /** + * Returns the time elapsed so far this week in milliseconds. + * + * @return Time elapsed this week in milliseconds. + */ + public long getElapsedWeek() { + // Today + days passed this week + long elapsed = getElapsedToday(); + + final int passedWeekdays = + calendar.get(Calendar.DAY_OF_WEEK) - 1 - calendar.getFirstDayOfWeek(); + if (passedWeekdays > 0) { + elapsed += passedWeekdays * MS_PER_DAY; } - /** - * Returns the time elapsed so far today in milliseconds. - * - * @return Time elapsed today in milliseconds. - */ - public long getElapsedToday() { - // Time elapsed so far today - return (calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE)) * MS_PER_MINUTE - + calendar.get(Calendar.SECOND) * 1000 - + calendar.get(Calendar.MILLISECOND); + return elapsed; + } + + /** + * Returns the time elapsed so far this month in milliseconds. + * + * @return Time elapsed this month in milliseconds. + */ + public long getElapsedMonth() { + // Today + rest of this month + return getElapsedToday() + ((calendar.get(Calendar.DAY_OF_MONTH) - 1) * MS_PER_DAY); + } + + /** + * Returns the time elapsed so far this month and the last numMonths months in milliseconds. + * + * @param numMonths Additional number of months prior to the current month to calculate. + * @return Time elapsed this month and the last numMonths months in milliseconds. + */ + public long getElapsedMonths(int numMonths) { + // Today + rest of this month + long elapsed = getElapsedMonth(); + + // Previous numMonths months + int month = calendar.get(Calendar.MONTH); + int year = calendar.get(Calendar.YEAR); + for (int i = 0; i < numMonths; i++) { + month--; + + if (month < Calendar.JANUARY) { + month = Calendar.DECEMBER; + year--; + } + + elapsed += getDaysInMonth(month) * MS_PER_DAY; } - /** - * Returns the time elapsed so far this week in milliseconds. - * - * @return Time elapsed this week in milliseconds. - */ - public long getElapsedWeek() { - // Today + days passed this week - long elapsed = getElapsedToday(); + return elapsed; + } - final int passedWeekdays = calendar.get(Calendar.DAY_OF_WEEK) - 1 - calendar.getFirstDayOfWeek(); - if (passedWeekdays > 0) { - elapsed += passedWeekdays * MS_PER_DAY; - } + /** + * Returns the time elapsed so far this year in milliseconds. + * + * @return Time elapsed this year in milliseconds. + */ + public long getElapsedYear() { + // Today + rest of this month + previous months until January + long elapsed = getElapsedMonth(); - return elapsed; + int month = calendar.get(Calendar.MONTH) - 1; + int year = calendar.get(Calendar.YEAR); + while (month > Calendar.JANUARY) { + elapsed += getDaysInMonth(month) * MS_PER_DAY; + + month--; } - /** - * Returns the time elapsed so far this month in milliseconds. - * - * @return Time elapsed this month in milliseconds. - */ - public long getElapsedMonth() { - // Today + rest of this month - return getElapsedToday() + - ((calendar.get(Calendar.DAY_OF_MONTH) - 1) * MS_PER_DAY); - } + return elapsed; + } - /** - * Returns the time elapsed so far this month and the last numMonths months in milliseconds. - * - * @param numMonths Additional number of months prior to the current month to calculate. - * @return Time elapsed this month and the last numMonths months in milliseconds. - */ - public long getElapsedMonths(int numMonths) { - // Today + rest of this month - long elapsed = getElapsedMonth(); + /** + * Gets the number of days for the given month in the given year. + * + * @param month The month (1 - 12). + * @return The days in that month/year. + */ + private int getDaysInMonth(int month) { + final Calendar monthCal = new GregorianCalendar(calendar.get(Calendar.YEAR), month, 1); + return monthCal.getActualMaximum(Calendar.DAY_OF_MONTH); + } - // Previous numMonths months - int month = calendar.get(Calendar.MONTH); - int year = calendar.get(Calendar.YEAR); - for (int i = 0; i < numMonths; i++) { - month--; + /** + * Returns the time elapsed so far last N days in milliseconds. + * + * @return Time elapsed since N days in milliseconds. + */ + public long getElapsedDays(int numDays) { + long elapsed = getElapsedToday(); + elapsed += numDays * MS_PER_DAY; - if (month < Calendar.JANUARY) { - month = Calendar.DECEMBER; - year--; - } - - elapsed += getDaysInMonth(month) * MS_PER_DAY; - } - - return elapsed; - } - - /** - * Returns the time elapsed so far this year in milliseconds. - * - * @return Time elapsed this year in milliseconds. - */ - public long getElapsedYear() { - // Today + rest of this month + previous months until January - long elapsed = getElapsedMonth(); - - int month = calendar.get(Calendar.MONTH) - 1; - int year = calendar.get(Calendar.YEAR); - while (month > Calendar.JANUARY) { - elapsed += getDaysInMonth(month) * MS_PER_DAY; - - month--; - } - - return elapsed; - } - - /** - * Gets the number of days for the given month in the given year. - * - * @param month The month (1 - 12). - * @return The days in that month/year. - */ - private int getDaysInMonth(int month) { - final Calendar monthCal = new GregorianCalendar(calendar.get(Calendar.YEAR), month, 1); - return monthCal.getActualMaximum(Calendar.DAY_OF_MONTH); - } - - /** - * Returns the time elapsed so far last N days in milliseconds. - * - * @return Time elapsed since N days in milliseconds. - */ - public long getElapsedDays(int numDays) { - long elapsed = getElapsedToday(); - elapsed += numDays * MS_PER_DAY; - - return elapsed; - } + return elapsed; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/ColorUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/ColorUtil.java index e4133392c..0f94d96a8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/ColorUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/ColorUtil.java @@ -1,58 +1,55 @@ package code.name.monkey.retromusic.util; import android.graphics.Bitmap; - import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.palette.graphics.Palette; - import java.util.Collections; import java.util.Comparator; public class ColorUtil { - @Nullable - public static Palette generatePalette(Bitmap bitmap) { - if (bitmap == null) return null; - return Palette.from(bitmap).generate(); + @Nullable + public static Palette generatePalette(Bitmap bitmap) { + if (bitmap == null) return null; + return Palette.from(bitmap).generate(); + } + + @ColorInt + public static int getColor(@Nullable Palette palette, int fallback) { + if (palette != null) { + if (palette.getVibrantSwatch() != null) { + return palette.getVibrantSwatch().getRgb(); + } else if (palette.getMutedSwatch() != null) { + return palette.getMutedSwatch().getRgb(); + } else if (palette.getDarkVibrantSwatch() != null) { + return palette.getDarkVibrantSwatch().getRgb(); + } else if (palette.getDarkMutedSwatch() != null) { + return palette.getDarkMutedSwatch().getRgb(); + } else if (palette.getLightVibrantSwatch() != null) { + return palette.getLightVibrantSwatch().getRgb(); + } else if (palette.getLightMutedSwatch() != null) { + return palette.getLightMutedSwatch().getRgb(); + } else if (!palette.getSwatches().isEmpty()) { + return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb(); + } + } + return fallback; + } + + private static class SwatchComparator implements Comparator { + private static SwatchComparator sInstance; + + static SwatchComparator getInstance() { + if (sInstance == null) { + sInstance = new SwatchComparator(); + } + return sInstance; } - @ColorInt - public static int getColor(@Nullable Palette palette, int fallback) { - if (palette != null) { - if (palette.getVibrantSwatch() != null) { - return palette.getVibrantSwatch().getRgb(); - } else if (palette.getMutedSwatch() != null) { - return palette.getMutedSwatch().getRgb(); - } else if (palette.getDarkVibrantSwatch() != null) { - return palette.getDarkVibrantSwatch().getRgb(); - } else if (palette.getDarkMutedSwatch() != null) { - return palette.getDarkMutedSwatch().getRgb(); - } else if (palette.getLightVibrantSwatch() != null) { - return palette.getLightVibrantSwatch().getRgb(); - } else if (palette.getLightMutedSwatch() != null) { - return palette.getLightMutedSwatch().getRgb(); - } else if (!palette.getSwatches().isEmpty()) { - return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb(); - } - } - return fallback; + @Override + public int compare(Palette.Swatch lhs, Palette.Swatch rhs) { + return lhs.getPopulation() - rhs.getPopulation(); } - - private static class SwatchComparator implements Comparator { - private static SwatchComparator sInstance; - - static SwatchComparator getInstance() { - if (sInstance == null) { - sInstance = new SwatchComparator(); - } - return sInstance; - } - - @Override - public int compare(Palette.Swatch lhs, Palette.Swatch rhs) { - return lhs.getPopulation() - rhs.getPopulation(); - } - } - -} \ No newline at end of file + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/Compressor.java b/app/src/main/java/code/name/monkey/retromusic/util/Compressor.java index 312a1f300..21050b74c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/Compressor.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/Compressor.java @@ -16,63 +16,64 @@ package code.name.monkey.retromusic.util; import android.content.Context; import android.graphics.Bitmap; - import java.io.File; import java.io.IOException; /** - * Created on : June 18, 2016 - * Author : zetbaitsu - * Name : Zetra - * GitHub : https://github.com/zetbaitsu + * Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : https://github.com/zetbaitsu */ public class Compressor { - //max width and height values of the compressed image is taken as 612x816 - private int maxWidth = 612; - private int maxHeight = 816; - private Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.JPEG; - private int quality = 80; - private String destinationDirectoryPath; + // max width and height values of the compressed image is taken as 612x816 + private int maxWidth = 612; + private int maxHeight = 816; + private Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.JPEG; + private int quality = 80; + private String destinationDirectoryPath; - public Compressor(Context context) { - destinationDirectoryPath = context.getCacheDir().getPath() + File.separator + "images"; - } + public Compressor(Context context) { + destinationDirectoryPath = context.getCacheDir().getPath() + File.separator + "images"; + } - public Compressor setMaxWidth(int maxWidth) { - this.maxWidth = maxWidth; - return this; - } + public Compressor setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + return this; + } - public Compressor setMaxHeight(int maxHeight) { - this.maxHeight = maxHeight; - return this; - } + public Compressor setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + return this; + } - public Compressor setCompressFormat(Bitmap.CompressFormat compressFormat) { - this.compressFormat = compressFormat; - return this; - } + public Compressor setCompressFormat(Bitmap.CompressFormat compressFormat) { + this.compressFormat = compressFormat; + return this; + } - public Compressor setQuality(int quality) { - this.quality = quality; - return this; - } + public Compressor setQuality(int quality) { + this.quality = quality; + return this; + } - public Compressor setDestinationDirectoryPath(String destinationDirectoryPath) { - this.destinationDirectoryPath = destinationDirectoryPath; - return this; - } + public Compressor setDestinationDirectoryPath(String destinationDirectoryPath) { + this.destinationDirectoryPath = destinationDirectoryPath; + return this; + } - public File compressToFile(File imageFile) throws IOException { - return compressToFile(imageFile, imageFile.getName()); - } + public File compressToFile(File imageFile) throws IOException { + return compressToFile(imageFile, imageFile.getName()); + } - public File compressToFile(File imageFile, String compressedFileName) throws IOException { - return ImageUtil.compressImage(imageFile, maxWidth, maxHeight, compressFormat, quality, - destinationDirectoryPath + File.separator + compressedFileName); - } + public File compressToFile(File imageFile, String compressedFileName) throws IOException { + return ImageUtil.compressImage( + imageFile, + maxWidth, + maxHeight, + compressFormat, + quality, + destinationDirectoryPath + File.separator + compressedFileName); + } - public Bitmap compressToBitmap(File imageFile) throws IOException { - return ImageUtil.decodeSampledBitmapFromFile(imageFile, maxWidth, maxHeight); - } + public Bitmap compressToBitmap(File imageFile) throws IOException { + return ImageUtil.decodeSampledBitmapFromFile(imageFile, maxWidth, maxHeight); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java index cbf13f2bb..9b3126e7f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java @@ -19,10 +19,11 @@ import android.database.Cursor; import android.os.Environment; import android.provider.MediaStore; import android.webkit.MimeTypeMap; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.repository.RealSongRepository; +import code.name.monkey.retromusic.repository.SortedCursor; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -36,228 +37,222 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import code.name.monkey.retromusic.model.Song; -import code.name.monkey.retromusic.repository.RealSongRepository; -import code.name.monkey.retromusic.repository.SortedCursor; - - public final class FileUtil { - private FileUtil() { + private FileUtil() {} + + public static byte[] readBytes(InputStream stream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int count; + while ((count = stream.read(buffer)) != -1) { + baos.write(buffer, 0, count); + } + stream.close(); + return baos.toByteArray(); + } + + @NonNull + public static List matchFilesWithMediaStore( + @NonNull Context context, @Nullable List files) { + return new RealSongRepository(context).songs(makeSongCursor(context, files)); + } + + public static String safeGetCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + e.printStackTrace(); + return file.getAbsolutePath(); + } + } + + @Nullable + public static SortedCursor makeSongCursor( + @NonNull final Context context, @Nullable final List files) { + String selection = null; + String[] paths = null; + + if (files != null) { + paths = toPathArray(files); + + if (files.size() > 0 + && files.size() < 999) { // 999 is the max amount Androids SQL implementation can handle. + selection = + MediaStore.Audio.AudioColumns.DATA + " IN (" + makePlaceholders(files.size()) + ")"; + } } - public static byte[] readBytes(InputStream stream) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[4096]; - int count; - while ((count = stream.read(buffer)) != -1) { - baos.write(buffer, 0, count); - } - stream.close(); - return baos.toByteArray(); - } + Cursor songCursor = + new RealSongRepository(context).makeSongCursor(selection, selection == null ? null : paths); - @NonNull - public static List matchFilesWithMediaStore(@NonNull Context context, - @Nullable List files) { - return new RealSongRepository(context).songs(makeSongCursor(context, files)); - } + return songCursor == null + ? null + : new SortedCursor(songCursor, paths, MediaStore.Audio.AudioColumns.DATA); + } - public static String safeGetCanonicalPath(File file) { - try { - return file.getCanonicalPath(); + private static String makePlaceholders(int len) { + StringBuilder sb = new StringBuilder(len * 2 - 1); + sb.append("?"); + for (int i = 1; i < len; i++) { + sb.append(",?"); + } + return sb.toString(); + } + + @Nullable + private static String[] toPathArray(@Nullable List files) { + if (files != null) { + String[] paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) { + /*try { + paths[i] = files.get(i).getCanonicalPath(); // canonical path is important here because we want to compare the path with the media store entry later } catch (IOException e) { e.printStackTrace(); - return file.getAbsolutePath(); - } + paths[i] = files.get(i).getPath(); + }*/ + paths[i] = safeGetCanonicalPath(files.get(i)); + } + return paths; } + return null; + } - @Nullable - public static SortedCursor makeSongCursor(@NonNull final Context context, - @Nullable final List files) { - String selection = null; - String[] paths = null; - - if (files != null) { - paths = toPathArray(files); - - if (files.size() > 0 - && files.size() < 999) { // 999 is the max amount Androids SQL implementation can handle. - selection = - MediaStore.Audio.AudioColumns.DATA + " IN (" + makePlaceholders(files.size()) + ")"; - } - } - - Cursor songCursor = new RealSongRepository(context).makeSongCursor(selection, selection == null ? null : paths); - - return songCursor == null ? null - : new SortedCursor(songCursor, paths, MediaStore.Audio.AudioColumns.DATA); + @NonNull + public static List listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) { + List fileList = new LinkedList<>(); + File[] found = directory.listFiles(fileFilter); + if (found != null) { + Collections.addAll(fileList, found); } + return fileList; + } - private static String makePlaceholders(int len) { - StringBuilder sb = new StringBuilder(len * 2 - 1); - sb.append("?"); - for (int i = 1; i < len; i++) { - sb.append(",?"); - } - return sb.toString(); + @NonNull + public static List listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) { + List files = new LinkedList<>(); + internalListFilesDeep(files, directory, fileFilter); + return files; + } + + @NonNull + public static List listFilesDeep( + @NonNull Collection files, @Nullable FileFilter fileFilter) { + List resFiles = new LinkedList<>(); + for (File file : files) { + if (file.isDirectory()) { + internalListFilesDeep(resFiles, file, fileFilter); + } else if (fileFilter == null || fileFilter.accept(file)) { + resFiles.add(file); + } } + return resFiles; + } - @Nullable - private static String[] toPathArray(@Nullable List files) { - if (files != null) { - String[] paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) { - /*try { - paths[i] = files.get(i).getCanonicalPath(); // canonical path is important here because we want to compare the path with the media store entry later - } catch (IOException e) { - e.printStackTrace(); - paths[i] = files.get(i).getPath(); - }*/ - paths[i] = safeGetCanonicalPath(files.get(i)); - } - return paths; - } - return null; - } + private static void internalListFilesDeep( + @NonNull Collection files, @NonNull File directory, @Nullable FileFilter fileFilter) { + File[] found = directory.listFiles(fileFilter); - @NonNull - public static List listFiles(@NonNull File directory, @Nullable FileFilter fileFilter) { - List fileList = new LinkedList<>(); - File[] found = directory.listFiles(fileFilter); - if (found != null) { - Collections.addAll(fileList, found); - } - return fileList; - } - - @NonNull - public static List listFilesDeep(@NonNull File directory, @Nullable FileFilter fileFilter) { - List files = new LinkedList<>(); - internalListFilesDeep(files, directory, fileFilter); - return files; - } - - @NonNull - public static List listFilesDeep(@NonNull Collection files, - @Nullable FileFilter fileFilter) { - List resFiles = new LinkedList<>(); - for (File file : files) { - if (file.isDirectory()) { - internalListFilesDeep(resFiles, file, fileFilter); - } else if (fileFilter == null || fileFilter.accept(file)) { - resFiles.add(file); - } - } - return resFiles; - } - - private static void internalListFilesDeep(@NonNull Collection files, - @NonNull File directory, @Nullable FileFilter fileFilter) { - File[] found = directory.listFiles(fileFilter); - - if (found != null) { - for (File file : found) { - if (file.isDirectory()) { - internalListFilesDeep(files, file, fileFilter); - } else { - files.add(file); - } - } - } - } - - public static boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) { - if (mimeType == null || mimeType.equals("*/*")) { - return true; + if (found != null) { + for (File file : found) { + if (file.isDirectory()) { + internalListFilesDeep(files, file, fileFilter); } else { - // get the file mime type - String filename = file.toURI().toString(); - int dotPos = filename.lastIndexOf('.'); - if (dotPos == -1) { - return false; - } - String fileExtension = filename.substring(dotPos + 1).toLowerCase(); - String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension); - if (fileType == null) { - return false; - } - // check the 'type/subtype' pattern - if (fileType.equals(mimeType)) { - return true; - } - // check the 'type/*' pattern - int mimeTypeDelimiter = mimeType.lastIndexOf('/'); - if (mimeTypeDelimiter == -1) { - return false; - } - String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter); - String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1); - if (!mimeTypeSubtype.equals("*")) { - return false; - } - int fileTypeDelimiter = fileType.lastIndexOf('/'); - if (fileTypeDelimiter == -1) { - return false; - } - String fileTypeMainType = fileType.substring(0, fileTypeDelimiter); - if (fileTypeMainType.equals(mimeTypeMainType)) { - return true; - } - return fileTypeMainType.equals(mimeTypeMainType); + files.add(file); } + } } + } - public static String stripExtension(String str) { - if (str == null) { - return null; - } - int pos = str.lastIndexOf('.'); - if (pos == -1) { - return str; - } - return str.substring(0, pos); + public static boolean fileIsMimeType(File file, String mimeType, MimeTypeMap mimeTypeMap) { + if (mimeType == null || mimeType.equals("*/*")) { + return true; + } else { + // get the file mime type + String filename = file.toURI().toString(); + int dotPos = filename.lastIndexOf('.'); + if (dotPos == -1) { + return false; + } + String fileExtension = filename.substring(dotPos + 1).toLowerCase(); + String fileType = mimeTypeMap.getMimeTypeFromExtension(fileExtension); + if (fileType == null) { + return false; + } + // check the 'type/subtype' pattern + if (fileType.equals(mimeType)) { + return true; + } + // check the 'type/*' pattern + int mimeTypeDelimiter = mimeType.lastIndexOf('/'); + if (mimeTypeDelimiter == -1) { + return false; + } + String mimeTypeMainType = mimeType.substring(0, mimeTypeDelimiter); + String mimeTypeSubtype = mimeType.substring(mimeTypeDelimiter + 1); + if (!mimeTypeSubtype.equals("*")) { + return false; + } + int fileTypeDelimiter = fileType.lastIndexOf('/'); + if (fileTypeDelimiter == -1) { + return false; + } + String fileTypeMainType = fileType.substring(0, fileTypeDelimiter); + if (fileTypeMainType.equals(mimeTypeMainType)) { + return true; + } + return fileTypeMainType.equals(mimeTypeMainType); } + } - public static String readFromStream(InputStream is) throws Exception { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - if (sb.length() > 0) { - sb.append("\n"); - } - sb.append(line); - } - reader.close(); - return sb.toString(); + public static String stripExtension(String str) { + if (str == null) { + return null; } - - public static String read(File file) throws Exception { - FileInputStream fin = new FileInputStream(file); - String ret = readFromStream(fin); - fin.close(); - return ret; + int pos = str.lastIndexOf('.'); + if (pos == -1) { + return str; } + return str.substring(0, pos); + } - public static boolean isExternalMemoryAvailable() { - Boolean isSDPresent = Environment.getExternalStorageState() - .equals(android.os.Environment.MEDIA_MOUNTED); - Boolean isSDSupportedDevice = Environment.isExternalStorageRemovable(); - - // yes SD-card is present - // Sorry - return isSDSupportedDevice && isSDPresent; + public static String readFromStream(InputStream is) throws Exception { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (sb.length() > 0) { + sb.append("\n"); + } + sb.append(line); } + reader.close(); + return sb.toString(); + } - public static File safeGetCanonicalFile(File file) { - try { - return file.getCanonicalFile(); - } catch (IOException e) { - e.printStackTrace(); - return file.getAbsoluteFile(); - } + public static String read(File file) throws Exception { + FileInputStream fin = new FileInputStream(file); + String ret = readFromStream(fin); + fin.close(); + return ret; + } + + public static boolean isExternalMemoryAvailable() { + Boolean isSDPresent = + Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED); + Boolean isSDSupportedDevice = Environment.isExternalStorageRemovable(); + + // yes SD-card is present + // Sorry + return isSDSupportedDevice && isSDPresent; + } + + public static File safeGetCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + e.printStackTrace(); + return file.getAbsoluteFile(); } - - + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java index 46b03a1bb..a4169fc14 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java @@ -26,262 +26,268 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.os.Build; - import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - +import code.name.monkey.appthemehelper.util.TintHelper; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import code.name.monkey.appthemehelper.util.TintHelper; - /** - * Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : - * https://github.com/zetbaitsu + * Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : https://github.com/zetbaitsu */ public class ImageUtil { - private static final int TOLERANCE = 20; - // Alpha amount for which values below are considered transparent. - private static final int ALPHA_TOLERANCE = 50; - private static int[] mTempBuffer; + private static final int TOLERANCE = 20; + // Alpha amount for which values below are considered transparent. + private static final int ALPHA_TOLERANCE = 50; + private static int[] mTempBuffer; - private ImageUtil() { + private ImageUtil() {} + public static boolean isGrayscale(Bitmap bitmap) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int size = height * width; + ensureBufferSize(size); + bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); + for (int i = 0; i < size; i++) { + if (!isGrayscale(mTempBuffer[i])) { + return false; + } + } + return true; + } + + public static Bitmap createBitmap(Drawable drawable) { + return createBitmap(drawable, 1f); + } + + public static Bitmap createBitmap(Drawable drawable, float sizeMultiplier) { + Bitmap bitmap = + Bitmap.createBitmap( + (int) (drawable.getIntrinsicWidth() * sizeMultiplier), + (int) (drawable.getIntrinsicHeight() * sizeMultiplier), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); + drawable.draw(c); + return bitmap; + } + + public static Drawable getTintedVectorDrawable( + @NonNull Resources res, + @DrawableRes int resId, + @Nullable Resources.Theme theme, + @ColorInt int color) { + return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color); + } + + public static Drawable getTintedVectorDrawable( + @NonNull Context context, @DrawableRes int id, @ColorInt int color) { + return TintHelper.createTintedDrawable( + getVectorDrawable(context.getResources(), id, context.getTheme()), color); + } + + public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int id) { + return getVectorDrawable(context.getResources(), id, context.getTheme()); + } + + public static Drawable getVectorDrawable( + @NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) { + if (Build.VERSION.SDK_INT >= 21) { + return res.getDrawable(resId, theme); + } + return VectorDrawableCompat.create(res, resId, theme); + } + + /** Makes sure that {@code mTempBuffer} has at least length {@code size}. */ + private static void ensureBufferSize(int size) { + if (mTempBuffer == null || mTempBuffer.length < size) { + mTempBuffer = new int[size]; + } + } + + public static Bitmap setBitmapColor(Bitmap bitmap, int color) { + Bitmap result = + Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1); + Paint paint = new Paint(); + paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + + Canvas canvas = new Canvas(result); + canvas.drawBitmap(result, 0, 0, paint); + + return result; + } + + public static boolean isGrayscale(int color) { + int alpha = 0xFF & (color >> 24); + if (alpha < ALPHA_TOLERANCE) { + return true; + } + int r = 0xFF & (color >> 16); + int g = 0xFF & (color >> 8); + int b = 0xFF & color; + return Math.abs(r - g) < TOLERANCE + && Math.abs(r - b) < TOLERANCE + && Math.abs(g - b) < TOLERANCE; + } // Amount (max is 255) that two channels can differ before the color is no longer "gray". + + public static Bitmap resizeBitmap(@NonNull Bitmap src, int maxForSmallerSize) { + int width = src.getWidth(); + int height = src.getHeight(); + + final int dstWidth; + final int dstHeight; + + if (width < height) { + if (maxForSmallerSize >= width) { + return src; + } + float ratio = (float) height / width; + dstWidth = maxForSmallerSize; + dstHeight = Math.round(maxForSmallerSize * ratio); + } else { + if (maxForSmallerSize >= height) { + return src; + } + float ratio = (float) width / height; + dstWidth = Math.round(maxForSmallerSize * ratio); + dstHeight = maxForSmallerSize; } - public static boolean isGrayscale(Bitmap bitmap) { - final int height = bitmap.getHeight(); - final int width = bitmap.getWidth(); - int size = height * width; - ensureBufferSize(size); - bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); - for (int i = 0; i < size; i++) { - if (!isGrayscale(mTempBuffer[i])) { - return false; - } - } - return true; + return Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false); + } + + public static int calculateInSampleSize(int width, int height, int reqWidth) { + // setting reqWidth matching to desired 1:1 ratio and screen-size + if (width < height) { + reqWidth = (height / width) * reqWidth; + } else { + reqWidth = (width / height) * reqWidth; } - public static Bitmap createBitmap(Drawable drawable) { - return createBitmap(drawable, 1f); + int inSampleSize = 1; + + if (height > reqWidth || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqWidth && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } } - public static Bitmap createBitmap(Drawable drawable, float sizeMultiplier) { - Bitmap bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * sizeMultiplier), (int) (drawable.getIntrinsicHeight() * sizeMultiplier), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(bitmap); - drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); - drawable.draw(c); - return bitmap; + return inSampleSize; + } + + static File compressImage( + File imageFile, + int reqWidth, + int reqHeight, + Bitmap.CompressFormat compressFormat, + int quality, + String destinationPath) + throws IOException { + FileOutputStream fileOutputStream = null; + File file = new File(destinationPath).getParentFile(); + if (!file.exists()) { + file.mkdirs(); + } + try { + fileOutputStream = new FileOutputStream(destinationPath); + // write the compressed bitmap at the destination specified by destinationPath. + decodeSampledBitmapFromFile(imageFile, reqWidth, reqHeight) + .compress(compressFormat, quality, fileOutputStream); + } finally { + if (fileOutputStream != null) { + fileOutputStream.flush(); + fileOutputStream.close(); + } } - public static Drawable getTintedVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme, @ColorInt int color) { - return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color); + return new File(destinationPath); + } + + static Bitmap decodeSampledBitmapFromFile(File imageFile, int reqWidth, int reqHeight) + throws IOException { + // First decode with inJustDecodeBounds=true to check dimensions + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + Bitmap scaledBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); + + // check the rotation of the image and display it properly + ExifInterface exif; + exif = new ExifInterface(imageFile.getAbsolutePath()); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); + Matrix matrix = new Matrix(); + if (orientation == 6) { + matrix.postRotate(90); + } else if (orientation == 3) { + matrix.postRotate(180); + } else if (orientation == 8) { + matrix.postRotate(270); + } + scaledBitmap = + Bitmap.createBitmap( + scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true); + return scaledBitmap; + } + + private static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } } - public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id, @ColorInt int color) { - return TintHelper.createTintedDrawable(getVectorDrawable(context.getResources(), id, context.getTheme()), color); + return inSampleSize; + } + + @NonNull + public static Bitmap getResizedBitmap(@NonNull Bitmap image, int maxSize) { + int width = image.getWidth(); + int height = image.getHeight(); + + float bitmapRatio = (float) width / (float) height; + if (bitmapRatio > 1) { + width = maxSize; + height = (int) (width / bitmapRatio); + } else { + height = maxSize; + width = (int) (height * bitmapRatio); } + return Bitmap.createScaledBitmap(image, width, height, true); + } - public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int id) { - return getVectorDrawable(context.getResources(), id, context.getTheme()); - } - - public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) { - if (Build.VERSION.SDK_INT >= 21) { - return res.getDrawable(resId, theme); - } - return VectorDrawableCompat.create(res, resId, theme); - } - - /** - * Makes sure that {@code mTempBuffer} has at least length {@code size}. - */ - private static void ensureBufferSize(int size) { - if (mTempBuffer == null || mTempBuffer.length < size) { - mTempBuffer = new int[size]; - } - } - - public static Bitmap setBitmapColor(Bitmap bitmap, int color) { - Bitmap result = Bitmap - .createBitmap(bitmap, 0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1); - Paint paint = new Paint(); - paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); - - Canvas canvas = new Canvas(result); - canvas.drawBitmap(result, 0, 0, paint); - - return result; - } - - public static boolean isGrayscale(int color) { - int alpha = 0xFF & (color >> 24); - if (alpha < ALPHA_TOLERANCE) { - return true; - } - int r = 0xFF & (color >> 16); - int g = 0xFF & (color >> 8); - int b = 0xFF & color; - return Math.abs(r - g) < TOLERANCE - && Math.abs(r - b) < TOLERANCE - && Math.abs(g - b) < TOLERANCE; - } // Amount (max is 255) that two channels can differ before the color is no longer "gray". - - public static Bitmap resizeBitmap(@NonNull Bitmap src, int maxForSmallerSize) { - int width = src.getWidth(); - int height = src.getHeight(); - - final int dstWidth; - final int dstHeight; - - if (width < height) { - if (maxForSmallerSize >= width) { - return src; - } - float ratio = (float) height / width; - dstWidth = maxForSmallerSize; - dstHeight = Math.round(maxForSmallerSize * ratio); - } else { - if (maxForSmallerSize >= height) { - return src; - } - float ratio = (float) width / height; - dstWidth = Math.round(maxForSmallerSize * ratio); - dstHeight = maxForSmallerSize; - } - - return Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false); - } - - public static int calculateInSampleSize(int width, int height, int reqWidth) { - // setting reqWidth matching to desired 1:1 ratio and screen-size - if (width < height) { - reqWidth = (height / width) * reqWidth; - } else { - reqWidth = (width / height) * reqWidth; - } - - int inSampleSize = 1; - - if (height > reqWidth || width > reqWidth) { - final int halfHeight = height / 2; - final int halfWidth = width / 2; - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((halfHeight / inSampleSize) > reqWidth - && (halfWidth / inSampleSize) > reqWidth) { - inSampleSize *= 2; - } - } - - return inSampleSize; - } - - static File compressImage(File imageFile, int reqWidth, int reqHeight, - Bitmap.CompressFormat compressFormat, int quality, String destinationPath) - throws IOException { - FileOutputStream fileOutputStream = null; - File file = new File(destinationPath).getParentFile(); - if (!file.exists()) { - file.mkdirs(); - } - try { - fileOutputStream = new FileOutputStream(destinationPath); - // write the compressed bitmap at the destination specified by destinationPath. - decodeSampledBitmapFromFile(imageFile, reqWidth, reqHeight) - .compress(compressFormat, quality, fileOutputStream); - } finally { - if (fileOutputStream != null) { - fileOutputStream.flush(); - fileOutputStream.close(); - } - } - - return new File(destinationPath); - } - - static Bitmap decodeSampledBitmapFromFile(File imageFile, int reqWidth, int reqHeight) - throws IOException { - // First decode with inJustDecodeBounds=true to check dimensions - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); - - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - - Bitmap scaledBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options); - - //check the rotation of the image and display it properly - ExifInterface exif; - exif = new ExifInterface(imageFile.getAbsolutePath()); - int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); - Matrix matrix = new Matrix(); - if (orientation == 6) { - matrix.postRotate(90); - } else if (orientation == 3) { - matrix.postRotate(180); - } else if (orientation == 8) { - matrix.postRotate(270); - } - scaledBitmap = Bitmap - .createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, - true); - return scaledBitmap; - } - - private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, - int reqHeight) { - // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; - - if (height > reqHeight || width > reqWidth) { - - final int halfHeight = height / 2; - final int halfWidth = width / 2; - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { - inSampleSize *= 2; - } - } - - return inSampleSize; - } - - @NonNull - public static Bitmap getResizedBitmap(@NonNull Bitmap image, int maxSize) { - int width = image.getWidth(); - int height = image.getHeight(); - - float bitmapRatio = (float) width / (float) height; - if (bitmapRatio > 1) { - width = maxSize; - height = (int) (width / bitmapRatio); - } else { - height = maxSize; - width = (int) (height * bitmapRatio); - } - return Bitmap.createScaledBitmap(image, width, height, true); - } - - public static Bitmap resize(InputStream stream, int scaledWidth, int scaledHeight) { - final Bitmap bitmap = BitmapFactory.decodeStream(stream); - return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true); - - } + public static Bitmap resize(InputStream stream, int scaledWidth, int scaledHeight) { + final Bitmap bitmap = BitmapFactory.decodeStream(stream); + return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java index ce1822171..8b5060646 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.java @@ -15,10 +15,8 @@ package code.name.monkey.retromusic.util; import android.util.Base64; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -28,110 +26,109 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -/** - * Created by hefuyi on 2016/11/8. - */ - +/** Created by hefuyi on 2016/11/8. */ public class LyricUtil { - private static final String lrcRootPath = android.os.Environment - .getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/"; - private static final String TAG = "LyricUtil"; + private static final String lrcRootPath = + android.os.Environment.getExternalStorageDirectory().toString() + "/RetroMusic/lyrics/"; + private static final String TAG = "LyricUtil"; - @Nullable - public static File writeLrcToLoc(@NonNull String title, @NonNull String artist, @NonNull String lrcContext) { - FileWriter writer = null; - try { - File file = new File(getLrcPath(title, artist)); - if (!file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - writer = new FileWriter(getLrcPath(title, artist)); - writer.write(lrcContext); - return file; - } catch (IOException e) { - e.printStackTrace(); - return null; - } finally { - try { - if (writer != null) - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } + @Nullable + public static File writeLrcToLoc( + @NonNull String title, @NonNull String artist, @NonNull String lrcContext) { + FileWriter writer = null; + try { + File file = new File(getLrcPath(title, artist)); + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + writer = new FileWriter(getLrcPath(title, artist)); + writer.write(lrcContext); + return file; + } catch (IOException e) { + e.printStackTrace(); + return null; + } finally { + try { + if (writer != null) writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } } + } - public static boolean deleteLrcFile(@NonNull String title, @NonNull String artist) { - File file = new File(getLrcPath(title, artist)); - return file.delete(); - } + public static boolean deleteLrcFile(@NonNull String title, @NonNull String artist) { + File file = new File(getLrcPath(title, artist)); + return file.delete(); + } - public static boolean isLrcFileExist(@NonNull String title, @NonNull String artist) { - File file = new File(getLrcPath(title, artist)); - return file.exists(); - } + public static boolean isLrcFileExist(@NonNull String title, @NonNull String artist) { + File file = new File(getLrcPath(title, artist)); + return file.exists(); + } - public static boolean isLrcOriginalFileExist(@NonNull String path) { - File file = new File(getLrcOriginalPath(path)); - return file.exists(); - } + public static boolean isLrcOriginalFileExist(@NonNull String path) { + File file = new File(getLrcOriginalPath(path)); + return file.exists(); + } - @Nullable - public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) { - File file = new File(getLrcPath(title, artist)); - if (file.exists()) { - return file; - } else { - return null; - } + @Nullable + public static File getLocalLyricFile(@NonNull String title, @NonNull String artist) { + File file = new File(getLrcPath(title, artist)); + if (file.exists()) { + return file; + } else { + return null; } + } - @Nullable - public static File getLocalLyricOriginalFile(@NonNull String path) { - File file = new File(getLrcOriginalPath(path)); - if (file.exists()) { - return file; - } else { - return null; - } + @Nullable + public static File getLocalLyricOriginalFile(@NonNull String path) { + File file = new File(getLrcOriginalPath(path)); + if (file.exists()) { + return file; + } else { + return null; } + } - private static String getLrcPath(String title, String artist) { - return lrcRootPath + title + " - " + artist + ".lrc"; - } + private static String getLrcPath(String title, String artist) { + return lrcRootPath + title + " - " + artist + ".lrc"; + } - private static String getLrcOriginalPath(String filePath) { - return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc"); - } + private static String getLrcOriginalPath(String filePath) { + return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc"); + } - @NonNull - public static String decryptBASE64(@NonNull String str) { - if (str == null || str.length() == 0) { - return null; - } - byte[] encode = str.getBytes(StandardCharsets.UTF_8); - // base64 解密 - return new String(Base64.decode(encode, 0, encode.length, Base64.DEFAULT), StandardCharsets.UTF_8); + @NonNull + public static String decryptBASE64(@NonNull String str) { + if (str == null || str.length() == 0) { + return null; } + byte[] encode = str.getBytes(StandardCharsets.UTF_8); + // base64 解密 + return new String( + Base64.decode(encode, 0, encode.length, Base64.DEFAULT), StandardCharsets.UTF_8); + } - @NonNull - public static String getStringFromFile(@NonNull String title, @NonNull String artist) throws Exception { - File file = new File(getLrcPath(title, artist)); - FileInputStream fin = new FileInputStream(file); - String ret = convertStreamToString(fin); - fin.close(); - return ret; - } + @NonNull + public static String getStringFromFile(@NonNull String title, @NonNull String artist) + throws Exception { + File file = new File(getLrcPath(title, artist)); + FileInputStream fin = new FileInputStream(file); + String ret = convertStreamToString(fin); + fin.close(); + return ret; + } - private static String convertStreamToString(InputStream is) throws Exception { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line = null; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); + private static String convertStreamToString(InputStream is) throws Exception { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); } + reader.close(); + return sb.toString(); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java index 406bb22b8..d1aa3c417 100755 --- a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.java @@ -21,12 +21,8 @@ import android.content.Context; import android.content.Intent; import android.media.audiofx.AudioEffect; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; - -import org.jetbrains.annotations.NotNull; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.DriveModeActivity; import code.name.monkey.retromusic.activities.LicenseActivity; @@ -38,71 +34,74 @@ import code.name.monkey.retromusic.activities.UserInfoActivity; import code.name.monkey.retromusic.activities.WhatsNewActivity; import code.name.monkey.retromusic.activities.bugreport.BugReportActivity; import code.name.monkey.retromusic.helper.MusicPlayerRemote; - +import org.jetbrains.annotations.NotNull; public class NavigationUtil { - public static void bugReport(@NonNull Activity activity) { - ActivityCompat.startActivity(activity, new Intent(activity, BugReportActivity.class), null); + public static void bugReport(@NonNull Activity activity) { + ActivityCompat.startActivity(activity, new Intent(activity, BugReportActivity.class), null); + } + + public static void goToLyrics(@NonNull Activity activity) { + Intent intent = new Intent(activity, LyricsActivity.class); + ActivityCompat.startActivity(activity, intent, null); + } + + public static void goToOpenSource(@NonNull Activity activity) { + ActivityCompat.startActivity(activity, new Intent(activity, LicenseActivity.class), null); + } + + public static void goToPlayingQueue(@NonNull Activity activity) { + Intent intent = new Intent(activity, PlayingQueueActivity.class); + ActivityCompat.startActivity(activity, intent, null); + } + + public static void goToProVersion(@NonNull Context context) { + ActivityCompat.startActivity(context, new Intent(context, PurchaseActivity.class), null); + } + + public static void goToSupportDevelopment(@NonNull Activity activity) { + ActivityCompat.startActivity( + activity, new Intent(activity, SupportDevelopmentActivity.class), null); + } + + public static void goToUserInfo( + @NonNull Activity activity, @NonNull ActivityOptions activityOptions) { + ActivityCompat.startActivity( + activity, new Intent(activity, UserInfoActivity.class), activityOptions.toBundle()); + } + + public static void gotoDriveMode(@NotNull final Activity activity) { + ActivityCompat.startActivity(activity, new Intent(activity, DriveModeActivity.class), null); + } + + public static void gotoWhatNews(@NonNull Activity activity) { + ActivityCompat.startActivity(activity, new Intent(activity, WhatsNewActivity.class), null); + } + + public static void openEqualizer(@NonNull final Activity activity) { + stockEqalizer(activity); + } + + private static void stockEqalizer(@NonNull Activity activity) { + final int sessionId = MusicPlayerRemote.INSTANCE.getAudioSessionId(); + if (sessionId == AudioEffect.ERROR_BAD_VALUE) { + Toast.makeText( + activity, activity.getResources().getString(R.string.no_audio_ID), Toast.LENGTH_LONG) + .show(); + } else { + try { + final Intent effects = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + effects.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId); + effects.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); + activity.startActivityForResult(effects, 0); + } catch (@NonNull final ActivityNotFoundException notFound) { + Toast.makeText( + activity, + activity.getResources().getString(R.string.no_equalizer), + Toast.LENGTH_SHORT) + .show(); + } } - - public static void goToLyrics(@NonNull Activity activity) { - Intent intent = new Intent(activity, LyricsActivity.class); - ActivityCompat.startActivity(activity, intent, null); - } - - public static void goToOpenSource(@NonNull Activity activity) { - ActivityCompat.startActivity(activity, new Intent(activity, LicenseActivity.class), null); - } - - public static void goToPlayingQueue(@NonNull Activity activity) { - Intent intent = new Intent(activity, PlayingQueueActivity.class); - ActivityCompat.startActivity(activity, intent, null); - } - - public static void goToProVersion(@NonNull Context context) { - ActivityCompat.startActivity(context, new Intent(context, PurchaseActivity.class), null); - } - - public static void goToSupportDevelopment(@NonNull Activity activity) { - ActivityCompat.startActivity(activity, new Intent(activity, SupportDevelopmentActivity.class), null); - } - - public static void goToUserInfo(@NonNull Activity activity, - @NonNull ActivityOptions activityOptions) { - ActivityCompat.startActivity(activity, new Intent(activity, UserInfoActivity.class), - activityOptions.toBundle()); - } - - public static void gotoDriveMode(@NotNull final Activity activity) { - ActivityCompat.startActivity(activity, new Intent(activity, DriveModeActivity.class), null); - } - - public static void gotoWhatNews(@NonNull Activity activity) { - ActivityCompat.startActivity(activity, new Intent(activity, WhatsNewActivity.class), null); - } - - public static void openEqualizer(@NonNull final Activity activity) { - stockEqalizer(activity); - } - - private static void stockEqalizer(@NonNull Activity activity) { - final int sessionId = MusicPlayerRemote.INSTANCE.getAudioSessionId(); - if (sessionId == AudioEffect.ERROR_BAD_VALUE) { - Toast.makeText(activity, activity.getResources().getString(R.string.no_audio_ID), - Toast.LENGTH_LONG).show(); - } else { - try { - final Intent effects = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); - effects.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId); - effects.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); - activity.startActivityForResult(effects, 0); - } catch (@NonNull final ActivityNotFoundException notFound) { - Toast.makeText(activity, activity.getResources().getString(R.string.no_equalizer), - Toast.LENGTH_SHORT).show(); - } - } - } - - + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java index 14ee82bd7..bd3a2c9d2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.util; +import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; + import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -23,259 +25,306 @@ import android.os.Environment; import android.provider.BaseColumns; import android.provider.MediaStore; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.db.PlaylistWithSongs; import code.name.monkey.retromusic.helper.M3UWriter; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.PlaylistSong; import code.name.monkey.retromusic.model.Song; - -import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class PlaylistsUtil { - public static long createPlaylist(@NonNull final Context context, @Nullable final String name) { - int id = -1; - if (name != null && name.length() > 0) { - try { - Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Audio.Playlists._ID}, - MediaStore.Audio.PlaylistsColumns.NAME + "=?", new String[]{name}, - null); - if (cursor == null || cursor.getCount() < 1) { - final ContentValues values = new ContentValues(1); - values.put(MediaStore.Audio.PlaylistsColumns.NAME, name); - final Uri uri = context.getContentResolver().insert( - EXTERNAL_CONTENT_URI, - values); - if (uri != null) { - // Necessary because somehow the MediaStoreObserver is not notified when adding a playlist - context.getContentResolver().notifyChange(Uri.parse("content://media"), null); - Toast.makeText(context, context.getResources().getString( - R.string.created_playlist_x, name), Toast.LENGTH_SHORT).show(); - id = Integer.parseInt(uri.getLastPathSegment()); - } - } else { - // Playlist exists - if (cursor.moveToFirst()) { - id = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Playlists._ID)); - } - } - if (cursor != null) { - cursor.close(); - } - } catch (SecurityException e) { - e.printStackTrace(); - } - } - if (id == -1) { - Toast.makeText(context, context.getResources().getString( - R.string.could_not_create_playlist), Toast.LENGTH_SHORT).show(); - } - return id; - } - - public static void deletePlaylists(@NonNull final Context context, @NonNull final List playlists) { - final StringBuilder selection = new StringBuilder(); - selection.append(MediaStore.Audio.Playlists._ID + " IN ("); - for (int i = 0; i < playlists.size(); i++) { - selection.append(playlists.get(i).getId()); - if (i < playlists.size() - 1) { - selection.append(","); - } - } - selection.append(")"); - try { - context.getContentResolver().delete(EXTERNAL_CONTENT_URI, selection.toString(), null); - context.getContentResolver().notifyChange(Uri.parse("content://media"), null); - } catch (SecurityException ignored) { - } - } - - public static void addToPlaylist(@NonNull final Context context, final Song song, final long playlistId, final boolean showToastOnFinish) { - List helperList = new ArrayList<>(); - helperList.add(song); - addToPlaylist(context, helperList, playlistId, showToastOnFinish); - } - - public static void addToPlaylist(@NonNull final Context context, @NonNull final List songs, final long playlistId, final boolean showToastOnFinish) { - final int size = songs.size(); - final ContentResolver resolver = context.getContentResolver(); - final String[] projection = new String[]{ - "max(" + MediaStore.Audio.Playlists.Members.PLAY_ORDER + ")", - }; - final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); - Cursor cursor = null; - int base = 0; - - try { - try { - cursor = resolver.query(uri, projection, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - base = cursor.getInt(0) + 1; - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - int numInserted = 0; - for (int offSet = 0; offSet < size; offSet += 1000) - numInserted += resolver.bulkInsert(uri, makeInsertItems(songs, offSet, 1000, base)); - - if (showToastOnFinish) { - Toast.makeText(context, context.getResources().getString( - R.string.inserted_x_songs_into_playlist_x, numInserted, getNameForPlaylist(context, playlistId)), Toast.LENGTH_SHORT).show(); - } - } catch (SecurityException ignored) { - ignored.printStackTrace(); - } - } - - @NonNull - public static ContentValues[] makeInsertItems(@NonNull final List songs, final int offset, int len, final int base) { - if (offset + len > songs.size()) { - len = songs.size() - offset; - } - - ContentValues[] contentValues = new ContentValues[len]; - - for (int i = 0; i < len; i++) { - contentValues[i] = new ContentValues(); - contentValues[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i); - contentValues[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, songs.get(offset + i).getId()); - } - return contentValues; - } - - public static String getNameForPlaylist(@NonNull final Context context, final long id) { - try { - Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Audio.PlaylistsColumns.NAME}, - BaseColumns._ID + "=?", - new String[]{String.valueOf(id)}, + public static long createPlaylist(@NonNull final Context context, @Nullable final String name) { + int id = -1; + if (name != null && name.length() > 0) { + try { + Cursor cursor = + context + .getContentResolver() + .query( + EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Audio.Playlists._ID}, + MediaStore.Audio.PlaylistsColumns.NAME + "=?", + new String[] {name}, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getString(0); - } - } finally { - cursor.close(); - } - } - } catch (SecurityException ignored) { - } - return ""; - } - - public static void removeFromPlaylist(@NonNull final Context context, @NonNull final Song song, long playlistId) { - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri( - "external", playlistId); - String selection = MediaStore.Audio.Playlists.Members.AUDIO_ID + " =?"; - String[] selectionArgs = new String[]{String.valueOf(song.getId())}; - - try { - context.getContentResolver().delete(uri, selection, selectionArgs); - } catch (SecurityException ignored) { - } - } - - public static void removeFromPlaylist(@NonNull final Context context, @NonNull final List songs) { - final long playlistId = songs.get(0).getPlaylistId(); - Uri uri = MediaStore.Audio.Playlists.Members.getContentUri( - "external", playlistId); - String[] selectionArgs = new String[songs.size()]; - for (int i = 0; i < selectionArgs.length; i++) { - selectionArgs[i] = String.valueOf(songs.get(i).getIdInPlayList()); - } - String selection = MediaStore.Audio.Playlists.Members._ID + " in ("; - //noinspection unused - for (String selectionArg : selectionArgs) selection += "?, "; - selection = selection.substring(0, selection.length() - 2) + ")"; - - try { - context.getContentResolver().delete(uri, selection, selectionArgs); - } catch (SecurityException ignored) { - } - } - - public static boolean doPlaylistContains(@NonNull final Context context, final long playlistId, final long songId) { - if (playlistId != -1) { - try { - Cursor c = context.getContentResolver().query( - MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), - new String[]{MediaStore.Audio.Playlists.Members.AUDIO_ID}, MediaStore.Audio.Playlists.Members.AUDIO_ID + "=?", new String[]{String.valueOf(songId)}, null); - int count = 0; - if (c != null) { - count = c.getCount(); - c.close(); - } - return count > 0; - } catch (SecurityException ignored) { - } - } - return false; - } - - public static boolean moveItem(@NonNull final Context context, long playlistId, int from, int to) { - return MediaStore.Audio.Playlists.Members.moveItem(context.getContentResolver(), - playlistId, from, to); - } - - public static void renamePlaylist(@NonNull final Context context, final long id, final String newName) { - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.Audio.PlaylistsColumns.NAME, newName); - try { - context.getContentResolver().update(EXTERNAL_CONTENT_URI, - contentValues, - MediaStore.Audio.Playlists._ID + "=?", - new String[]{String.valueOf(id)}); + if (cursor == null || cursor.getCount() < 1) { + final ContentValues values = new ContentValues(1); + values.put(MediaStore.Audio.PlaylistsColumns.NAME, name); + final Uri uri = context.getContentResolver().insert(EXTERNAL_CONTENT_URI, values); + if (uri != null) { + // Necessary because somehow the MediaStoreObserver is not notified when adding a + // playlist context.getContentResolver().notifyChange(Uri.parse("content://media"), null); - } catch (SecurityException ignored) { + Toast.makeText( + context, + context.getResources().getString(R.string.created_playlist_x, name), + Toast.LENGTH_SHORT) + .show(); + id = Integer.parseInt(uri.getLastPathSegment()); + } + } else { + // Playlist exists + if (cursor.moveToFirst()) { + id = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Playlists._ID)); + } } - } - - public static File savePlaylist(Context context, Playlist playlist) throws IOException { - return M3UWriter.write(new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); - } - - public static File savePlaylistWithSongs(PlaylistWithSongs playlist) throws IOException { - return M3UWriter.writeIO(new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); - } - - public static boolean doesPlaylistExist(@NonNull final Context context, final int playlistId) { - return playlistId != -1 && doesPlaylistExist(context, - MediaStore.Audio.Playlists._ID + "=?", - new String[]{String.valueOf(playlistId)}); - } - - public static boolean doesPlaylistExist(@NonNull final Context context, final String name) { - return doesPlaylistExist(context, - MediaStore.Audio.PlaylistsColumns.NAME + "=?", - new String[]{name}); - } - - private static boolean doesPlaylistExist(@NonNull Context context, @NonNull final String selection, @NonNull final String[] values) { - Cursor cursor = context.getContentResolver().query(EXTERNAL_CONTENT_URI, - new String[]{}, selection, values, null); - - boolean exists = false; if (cursor != null) { - exists = cursor.getCount() != 0; - cursor.close(); + cursor.close(); } - return exists; + } catch (SecurityException e) { + e.printStackTrace(); + } } -} \ No newline at end of file + if (id == -1) { + Toast.makeText( + context, + context.getResources().getString(R.string.could_not_create_playlist), + Toast.LENGTH_SHORT) + .show(); + } + return id; + } + + public static void deletePlaylists( + @NonNull final Context context, @NonNull final List playlists) { + final StringBuilder selection = new StringBuilder(); + selection.append(MediaStore.Audio.Playlists._ID + " IN ("); + for (int i = 0; i < playlists.size(); i++) { + selection.append(playlists.get(i).getId()); + if (i < playlists.size() - 1) { + selection.append(","); + } + } + selection.append(")"); + try { + context.getContentResolver().delete(EXTERNAL_CONTENT_URI, selection.toString(), null); + context.getContentResolver().notifyChange(Uri.parse("content://media"), null); + } catch (SecurityException ignored) { + } + } + + public static void addToPlaylist( + @NonNull final Context context, + final Song song, + final long playlistId, + final boolean showToastOnFinish) { + List helperList = new ArrayList<>(); + helperList.add(song); + addToPlaylist(context, helperList, playlistId, showToastOnFinish); + } + + public static void addToPlaylist( + @NonNull final Context context, + @NonNull final List songs, + final long playlistId, + final boolean showToastOnFinish) { + final int size = songs.size(); + final ContentResolver resolver = context.getContentResolver(); + final String[] projection = + new String[] { + "max(" + MediaStore.Audio.Playlists.Members.PLAY_ORDER + ")", + }; + final Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); + Cursor cursor = null; + int base = 0; + + try { + try { + cursor = resolver.query(uri, projection, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + base = cursor.getInt(0) + 1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + int numInserted = 0; + for (int offSet = 0; offSet < size; offSet += 1000) + numInserted += resolver.bulkInsert(uri, makeInsertItems(songs, offSet, 1000, base)); + + if (showToastOnFinish) { + Toast.makeText( + context, + context + .getResources() + .getString( + R.string.inserted_x_songs_into_playlist_x, + numInserted, + getNameForPlaylist(context, playlistId)), + Toast.LENGTH_SHORT) + .show(); + } + } catch (SecurityException ignored) { + ignored.printStackTrace(); + } + } + + @NonNull + public static ContentValues[] makeInsertItems( + @NonNull final List songs, final int offset, int len, final int base) { + if (offset + len > songs.size()) { + len = songs.size() - offset; + } + + ContentValues[] contentValues = new ContentValues[len]; + + for (int i = 0; i < len; i++) { + contentValues[i] = new ContentValues(); + contentValues[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + offset + i); + contentValues[i].put( + MediaStore.Audio.Playlists.Members.AUDIO_ID, songs.get(offset + i).getId()); + } + return contentValues; + } + + public static String getNameForPlaylist(@NonNull final Context context, final long id) { + try { + Cursor cursor = + context + .getContentResolver() + .query( + EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Audio.PlaylistsColumns.NAME}, + BaseColumns._ID + "=?", + new String[] {String.valueOf(id)}, + null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + return cursor.getString(0); + } + } finally { + cursor.close(); + } + } + } catch (SecurityException ignored) { + } + return ""; + } + + public static void removeFromPlaylist( + @NonNull final Context context, @NonNull final Song song, long playlistId) { + Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); + String selection = MediaStore.Audio.Playlists.Members.AUDIO_ID + " =?"; + String[] selectionArgs = new String[] {String.valueOf(song.getId())}; + + try { + context.getContentResolver().delete(uri, selection, selectionArgs); + } catch (SecurityException ignored) { + } + } + + public static void removeFromPlaylist( + @NonNull final Context context, @NonNull final List songs) { + final long playlistId = songs.get(0).getPlaylistId(); + Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); + String[] selectionArgs = new String[songs.size()]; + for (int i = 0; i < selectionArgs.length; i++) { + selectionArgs[i] = String.valueOf(songs.get(i).getIdInPlayList()); + } + String selection = MediaStore.Audio.Playlists.Members._ID + " in ("; + //noinspection unused + for (String selectionArg : selectionArgs) selection += "?, "; + selection = selection.substring(0, selection.length() - 2) + ")"; + + try { + context.getContentResolver().delete(uri, selection, selectionArgs); + } catch (SecurityException ignored) { + } + } + + public static boolean doPlaylistContains( + @NonNull final Context context, final long playlistId, final long songId) { + if (playlistId != -1) { + try { + Cursor c = + context + .getContentResolver() + .query( + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId), + new String[] {MediaStore.Audio.Playlists.Members.AUDIO_ID}, + MediaStore.Audio.Playlists.Members.AUDIO_ID + "=?", + new String[] {String.valueOf(songId)}, + null); + int count = 0; + if (c != null) { + count = c.getCount(); + c.close(); + } + return count > 0; + } catch (SecurityException ignored) { + } + } + return false; + } + + public static boolean moveItem( + @NonNull final Context context, long playlistId, int from, int to) { + return MediaStore.Audio.Playlists.Members.moveItem( + context.getContentResolver(), playlistId, from, to); + } + + public static void renamePlaylist( + @NonNull final Context context, final long id, final String newName) { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Audio.PlaylistsColumns.NAME, newName); + try { + context + .getContentResolver() + .update( + EXTERNAL_CONTENT_URI, + contentValues, + MediaStore.Audio.Playlists._ID + "=?", + new String[] {String.valueOf(id)}); + context.getContentResolver().notifyChange(Uri.parse("content://media"), null); + } catch (SecurityException ignored) { + } + } + + public static File savePlaylist(Context context, Playlist playlist) throws IOException { + return M3UWriter.write( + new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); + } + + public static File savePlaylistWithSongs(PlaylistWithSongs playlist) throws IOException { + return M3UWriter.writeIO( + new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); + } + + public static boolean doesPlaylistExist(@NonNull final Context context, final int playlistId) { + return playlistId != -1 + && doesPlaylistExist( + context, + MediaStore.Audio.Playlists._ID + "=?", + new String[] {String.valueOf(playlistId)}); + } + + public static boolean doesPlaylistExist(@NonNull final Context context, final String name) { + return doesPlaylistExist( + context, MediaStore.Audio.PlaylistsColumns.NAME + "=?", new String[] {name}); + } + + private static boolean doesPlaylistExist( + @NonNull Context context, @NonNull final String selection, @NonNull final String[] values) { + Cursor cursor = + context + .getContentResolver() + .query(EXTERNAL_CONTENT_URI, new String[] {}, selection, values, null); + + boolean exists = false; + if (cursor != null) { + exists = cursor.getCount() != 0; + cursor.close(); + } + return exists; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/RetroColorUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/RetroColorUtil.java index 310c76fa7..47fbdd8b9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/RetroColorUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/RetroColorUtil.java @@ -18,203 +18,204 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.palette.graphics.Palette; - +import code.name.monkey.appthemehelper.util.ColorUtil; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import code.name.monkey.appthemehelper.util.ColorUtil; - public class RetroColorUtil { - public static int desaturateColor(int color, float ratio) { - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); + public static int desaturateColor(int color, float ratio) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); - hsv[1] = (hsv[1] / 1 * ratio) + (0.2f * (1.0f - ratio)); + hsv[1] = (hsv[1] / 1 * ratio) + (0.2f * (1.0f - ratio)); - return Color.HSVToColor(hsv); + return Color.HSVToColor(hsv); + } + + @Nullable + public static Palette generatePalette(@Nullable Bitmap bitmap) { + return bitmap == null ? null : Palette.from(bitmap).clearFilters().generate(); + } + + public static int getTextColor(@Nullable Palette palette) { + if (palette == null) { + return -1; } - @Nullable - public static Palette generatePalette(@Nullable Bitmap bitmap) { - return bitmap == null ? null : Palette.from(bitmap).clearFilters().generate(); + int inverse = -1; + if (palette.getVibrantSwatch() != null) { + inverse = palette.getVibrantSwatch().getRgb(); + } else if (palette.getLightVibrantSwatch() != null) { + inverse = palette.getLightVibrantSwatch().getRgb(); + } else if (palette.getDarkVibrantSwatch() != null) { + inverse = palette.getDarkVibrantSwatch().getRgb(); } - public static int getTextColor(@Nullable Palette palette) { - if (palette == null) { - return -1; - } + int background = getSwatch(palette).getRgb(); - int inverse = -1; - if (palette.getVibrantSwatch() != null) { - inverse = palette.getVibrantSwatch().getRgb(); - } else if (palette.getLightVibrantSwatch() != null) { - inverse = palette.getLightVibrantSwatch().getRgb(); - } else if (palette.getDarkVibrantSwatch() != null) { - inverse = palette.getDarkVibrantSwatch().getRgb(); - } - - int background = getSwatch(palette).getRgb(); - - if (inverse != -1) { - return ColorUtil.INSTANCE.getReadableText(inverse, background, 150); - } - return ColorUtil.INSTANCE.stripAlpha(getSwatch(palette).getTitleTextColor()); + if (inverse != -1) { + return ColorUtil.INSTANCE.getReadableText(inverse, background, 150); } + return ColorUtil.INSTANCE.stripAlpha(getSwatch(palette).getTitleTextColor()); + } - @NonNull - public static Palette.Swatch getSwatch(@Nullable Palette palette) { - if (palette == null) { - return new Palette.Swatch(Color.WHITE, 1); - } + @NonNull + public static Palette.Swatch getSwatch(@Nullable Palette palette) { + if (palette == null) { + return new Palette.Swatch(Color.WHITE, 1); + } + return getBestPaletteSwatchFrom(palette.getSwatches()); + } + + public static int getMatColor(Context context, String typeColor) { + int returnColor = Color.BLACK; + int arrayId = + context + .getResources() + .getIdentifier( + "md_" + typeColor, "array", context.getApplicationContext().getPackageName()); + + if (arrayId != 0) { + TypedArray colors = context.getResources().obtainTypedArray(arrayId); + int index = (int) (Math.random() * colors.length()); + returnColor = colors.getColor(index, Color.BLACK); + colors.recycle(); + } + return returnColor; + } + + @ColorInt + public static int getColor(@Nullable Palette palette, int fallback) { + if (palette != null) { + if (palette.getVibrantSwatch() != null) { + return palette.getVibrantSwatch().getRgb(); + } else if (palette.getDarkVibrantSwatch() != null) { + return palette.getDarkVibrantSwatch().getRgb(); + } else if (palette.getLightVibrantSwatch() != null) { + return palette.getLightVibrantSwatch().getRgb(); + } else if (palette.getMutedSwatch() != null) { + return palette.getMutedSwatch().getRgb(); + } else if (palette.getLightMutedSwatch() != null) { + return palette.getLightMutedSwatch().getRgb(); + } else if (palette.getDarkMutedSwatch() != null) { + return palette.getDarkMutedSwatch().getRgb(); + } else if (!palette.getSwatches().isEmpty()) { + return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb(); + } + } + return fallback; + } + + private static Palette.Swatch getTextSwatch(@Nullable Palette palette) { + if (palette == null) { + return new Palette.Swatch(Color.BLACK, 1); + } + if (palette.getVibrantSwatch() != null) { + return palette.getVibrantSwatch(); + } else { + return new Palette.Swatch(Color.BLACK, 1); + } + } + + @ColorInt + public static int getBackgroundColor(@Nullable Palette palette) { + return getProperBackgroundSwatch(palette).getRgb(); + } + + private static Palette.Swatch getProperBackgroundSwatch(@Nullable Palette palette) { + if (palette == null) { + return new Palette.Swatch(Color.BLACK, 1); + } + if (palette.getDarkMutedSwatch() != null) { + return palette.getDarkMutedSwatch(); + } else if (palette.getMutedSwatch() != null) { + return palette.getMutedSwatch(); + } else if (palette.getLightMutedSwatch() != null) { + return palette.getLightMutedSwatch(); + } else { + return new Palette.Swatch(Color.BLACK, 1); + } + } + + private static Palette.Swatch getBestPaletteSwatchFrom(Palette palette) { + if (palette != null) { + if (palette.getVibrantSwatch() != null) { + return palette.getVibrantSwatch(); + } else if (palette.getMutedSwatch() != null) { + return palette.getMutedSwatch(); + } else if (palette.getDarkVibrantSwatch() != null) { + return palette.getDarkVibrantSwatch(); + } else if (palette.getDarkMutedSwatch() != null) { + return palette.getDarkMutedSwatch(); + } else if (palette.getLightVibrantSwatch() != null) { + return palette.getLightVibrantSwatch(); + } else if (palette.getLightMutedSwatch() != null) { + return palette.getLightMutedSwatch(); + } else if (!palette.getSwatches().isEmpty()) { return getBestPaletteSwatchFrom(palette.getSwatches()); - + } } + return null; + } - public static int getMatColor(Context context, String typeColor) { - int returnColor = Color.BLACK; - int arrayId = context.getResources().getIdentifier("md_" + typeColor, "array", - context.getApplicationContext().getPackageName()); - - if (arrayId != 0) { - TypedArray colors = context.getResources().obtainTypedArray(arrayId); - int index = (int) (Math.random() * colors.length()); - returnColor = colors.getColor(index, Color.BLACK); - colors.recycle(); - } - return returnColor; + private static Palette.Swatch getBestPaletteSwatchFrom(List swatches) { + if (swatches == null) { + return null; } - - @ColorInt - public static int getColor(@Nullable Palette palette, int fallback) { - if (palette != null) { - if (palette.getVibrantSwatch() != null) { - return palette.getVibrantSwatch().getRgb(); - } else if (palette.getDarkVibrantSwatch() != null) { - return palette.getDarkVibrantSwatch().getRgb(); - } else if (palette.getLightVibrantSwatch() != null) { - return palette.getLightVibrantSwatch().getRgb(); - } else if (palette.getMutedSwatch() != null) { - return palette.getMutedSwatch().getRgb(); - } else if (palette.getLightMutedSwatch() != null) { - return palette.getLightMutedSwatch().getRgb(); - } else if (palette.getDarkMutedSwatch() != null) { - return palette.getDarkMutedSwatch().getRgb(); - } else if (!palette.getSwatches().isEmpty()) { - return Collections.max(palette.getSwatches(), SwatchComparator.getInstance()).getRgb(); - } - } - return fallback; - } - - private static Palette.Swatch getTextSwatch(@Nullable Palette palette) { - if (palette == null) { - return new Palette.Swatch(Color.BLACK, 1); - } - if (palette.getVibrantSwatch() != null) { - return palette.getVibrantSwatch(); - } else { - return new Palette.Swatch(Color.BLACK, 1); - } - } - - @ColorInt - public static int getBackgroundColor(@Nullable Palette palette) { - return getProperBackgroundSwatch(palette).getRgb(); - } - - private static Palette.Swatch getProperBackgroundSwatch(@Nullable Palette palette) { - if (palette == null) { - return new Palette.Swatch(Color.BLACK, 1); - } - if (palette.getDarkMutedSwatch() != null) { - return palette.getDarkMutedSwatch(); - } else if (palette.getMutedSwatch() != null) { - return palette.getMutedSwatch(); - } else if (palette.getLightMutedSwatch() != null) { - return palette.getLightMutedSwatch(); - } else { - return new Palette.Swatch(Color.BLACK, 1); - } - } - - private static Palette.Swatch getBestPaletteSwatchFrom(Palette palette) { - if (palette != null) { - if (palette.getVibrantSwatch() != null) { - return palette.getVibrantSwatch(); - } else if (palette.getMutedSwatch() != null) { - return palette.getMutedSwatch(); - } else if (palette.getDarkVibrantSwatch() != null) { - return palette.getDarkVibrantSwatch(); - } else if (palette.getDarkMutedSwatch() != null) { - return palette.getDarkMutedSwatch(); - } else if (palette.getLightVibrantSwatch() != null) { - return palette.getLightVibrantSwatch(); - } else if (palette.getLightMutedSwatch() != null) { - return palette.getLightMutedSwatch(); - } else if (!palette.getSwatches().isEmpty()) { - return getBestPaletteSwatchFrom(palette.getSwatches()); - } - } - return null; - } - - private static Palette.Swatch getBestPaletteSwatchFrom(List swatches) { - if (swatches == null) { - return null; - } - return Collections.max(swatches, (opt1, opt2) -> { - int a = opt1 == null ? 0 : opt1.getPopulation(); - int b = opt2 == null ? 0 : opt2.getPopulation(); - return a - b; + return Collections.max( + swatches, + (opt1, opt2) -> { + int a = opt1 == null ? 0 : opt1.getPopulation(); + int b = opt2 == null ? 0 : opt2.getPopulation(); + return a - b; }); + } + + public static int getDominantColor(Bitmap bitmap, int defaultFooterColor) { + List swatchesTemp = Palette.from(bitmap).generate().getSwatches(); + List swatches = new ArrayList(swatchesTemp); + Collections.sort( + swatches, (swatch1, swatch2) -> swatch2.getPopulation() - swatch1.getPopulation()); + return swatches.size() > 0 ? swatches.get(0).getRgb() : defaultFooterColor; + } + + @ColorInt + public static int shiftBackgroundColorForLightText(@ColorInt int backgroundColor) { + while (ColorUtil.INSTANCE.isColorLight(backgroundColor)) { + backgroundColor = ColorUtil.INSTANCE.darkenColor(backgroundColor); + } + return backgroundColor; + } + + @ColorInt + public static int shiftBackgroundColorForDarkText(@ColorInt int backgroundColor) { + int color = backgroundColor; + while (!ColorUtil.INSTANCE.isColorLight(backgroundColor)) { + color = ColorUtil.INSTANCE.lightenColor(backgroundColor); + } + return color; + } + + private static class SwatchComparator implements Comparator { + + private static SwatchComparator sInstance; + + static SwatchComparator getInstance() { + if (sInstance == null) { + sInstance = new SwatchComparator(); + } + return sInstance; } - - public static int getDominantColor(Bitmap bitmap, int defaultFooterColor) { - List swatchesTemp = Palette.from(bitmap).generate().getSwatches(); - List swatches = new ArrayList(swatchesTemp); - Collections.sort(swatches, (swatch1, swatch2) -> swatch2.getPopulation() - swatch1.getPopulation()); - return swatches.size() > 0 ? swatches.get(0).getRgb() : defaultFooterColor; - } - - @ColorInt - public static int shiftBackgroundColorForLightText(@ColorInt int backgroundColor) { - while (ColorUtil.INSTANCE.isColorLight(backgroundColor)) { - backgroundColor = ColorUtil.INSTANCE.darkenColor(backgroundColor); - } - return backgroundColor; - } - - @ColorInt - public static int shiftBackgroundColorForDarkText(@ColorInt int backgroundColor) { - int color = backgroundColor; - while (!ColorUtil.INSTANCE.isColorLight(backgroundColor)) { - color = ColorUtil.INSTANCE.lightenColor(backgroundColor); - } - return color; - } - - private static class SwatchComparator implements Comparator { - - private static SwatchComparator sInstance; - - static SwatchComparator getInstance() { - if (sInstance == null) { - sInstance = new SwatchComparator(); - } - return sInstance; - } - - @Override - public int compare(Palette.Swatch lhs, Palette.Swatch rhs) { - return lhs.getPopulation() - rhs.getPopulation(); - } + @Override + public int compare(Palette.Swatch lhs, Palette.Swatch rhs) { + return lhs.getPopulation() - rhs.getPopulation(); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java index f1b1c6654..61630ce2a 100755 --- a/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java @@ -35,165 +35,176 @@ import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; - import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import java.text.DecimalFormat; - import code.name.monkey.appthemehelper.util.TintHelper; import code.name.monkey.retromusic.App; +import java.text.DecimalFormat; public class RetroUtil { - private static final int[] TEMP_ARRAY = new int[1]; + private static final int[] TEMP_ARRAY = new int[1]; - private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar"; + private static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar"; - public static int calculateNoOfColumns(@NonNull Context context) { - DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - float dpWidth = displayMetrics.widthPixels / displayMetrics.density; - return (int) (dpWidth / 180); + public static int calculateNoOfColumns(@NonNull Context context) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + float dpWidth = displayMetrics.widthPixels / displayMetrics.density; + return (int) (dpWidth / 180); + } + + @NonNull + public static Bitmap createBitmap(@NonNull Drawable drawable, float sizeMultiplier) { + Bitmap bitmap = + Bitmap.createBitmap( + (int) (drawable.getIntrinsicWidth() * sizeMultiplier), + (int) (drawable.getIntrinsicHeight() * sizeMultiplier), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); + drawable.draw(c); + return bitmap; + } + + public static String formatValue(float value) { + String[] arr = {"", "K", "M", "B", "T", "P", "E"}; + int index = 0; + while ((value / 1000) >= 1) { + value = value / 1000; + index++; } + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + return String.format("%s %s", decimalFormat.format(value), arr[index]); + } - @NonNull - public static Bitmap createBitmap(@NonNull Drawable drawable, float sizeMultiplier) { - Bitmap bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * sizeMultiplier), - (int) (drawable.getIntrinsicHeight() * sizeMultiplier), Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(bitmap); - drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); - drawable.draw(c); - return bitmap; + public static float frequencyCount(int frequency) { + return (float) (frequency / 1000.0); + } + + public static Point getScreenSize(@NonNull Context c) { + Display display = null; + if (c.getSystemService(Context.WINDOW_SERVICE) != null) { + display = ((WindowManager) c.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); } + Point size = new Point(); + if (display != null) { + display.getSize(size); + } + return size; + } - public static String formatValue(float value) { - String[] arr = {"", "K", "M", "B", "T", "P", "E"}; - int index = 0; - while ((value / 1000) >= 1) { - value = value / 1000; - index++; + public static int getStatusBarHeight() { + int result = 0; + int resourceId = + App.Companion.getContext() + .getResources() + .getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = App.Companion.getContext().getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + @Nullable + public static Drawable getTintedVectorDrawable( + @NonNull Context context, @DrawableRes int id, @ColorInt int color) { + return TintHelper.createTintedDrawable( + getVectorDrawable(context.getResources(), id, context.getTheme()), color); + } + + @Nullable + public static Drawable getTintedVectorDrawable( + @NonNull Resources res, + @DrawableRes int resId, + @Nullable Resources.Theme theme, + @ColorInt int color) { + return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color); + } + + @Nullable + public static Drawable getVectorDrawable( + @NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) { + if (Build.VERSION.SDK_INT >= 21) { + return res.getDrawable(resId, theme); + } + return VectorDrawableCompat.create(res, resId, theme); + } + + public static void hideSoftKeyboard(@Nullable Activity activity) { + if (activity != null) { + View currentFocus = activity.getCurrentFocus(); + if (currentFocus != null) { + InputMethodManager inputMethodManager = + (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); } - DecimalFormat decimalFormat = new DecimalFormat("#.##"); - return String.format("%s %s", decimalFormat.format(value), arr[index]); + } } + } - public static float frequencyCount(int frequency) { - return (float) (frequency / 1000.0); + public static boolean isAllowedToDownloadMetadata(final @NonNull Context context) { + switch (PreferenceUtil.INSTANCE.getAutoDownloadImagesPolicy()) { + case "always": + return true; + case "only_wifi": + final ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); + return netInfo != null + && netInfo.getType() == ConnectivityManager.TYPE_WIFI + && netInfo.isConnectedOrConnecting(); + case "never": + default: + return false; } + } - public static Point getScreenSize(@NonNull Context c) { - Display display = null; - if (c.getSystemService(Context.WINDOW_SERVICE) != null) { - display = ((WindowManager) c.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - } - Point size = new Point(); - if (display != null) { - display.getSize(size); - } - return size; - } + public static boolean isLandscape() { + return App.Companion.getContext().getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } - public static int getStatusBarHeight() { - int result = 0; - int resourceId = App.Companion.getContext().getResources() - .getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - result = App.Companion.getContext().getResources().getDimensionPixelSize(resourceId); - } - return result; - } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isRTL(@NonNull Context context) { + Configuration config = context.getResources().getConfiguration(); + return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } - @Nullable - public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id, - @ColorInt int color) { - return TintHelper.createTintedDrawable( - getVectorDrawable(context.getResources(), id, context.getTheme()), color); - } + public static boolean isTablet() { + return App.Companion.getContext().getResources().getConfiguration().smallestScreenWidthDp + >= 600; + } - @Nullable - public static Drawable getTintedVectorDrawable(@NonNull Resources res, @DrawableRes int resId, - @Nullable Resources.Theme theme, @ColorInt int color) { - return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color); - } + public static void openUrl(@NonNull Activity context, @NonNull String str) { + Intent intent = new Intent("android.intent.action.VIEW"); + intent.setData(Uri.parse(str)); + intent.setFlags(268435456); + context.startActivity(intent); + } - @Nullable - public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId, - @Nullable Resources.Theme theme) { - if (Build.VERSION.SDK_INT >= 21) { - return res.getDrawable(resId, theme); - } - return VectorDrawableCompat.create(res, resId, theme); - } + public static void setAllowDrawUnderNavigationBar(Window window) { + window.setNavigationBarColor(Color.TRANSPARENT); + window + .getDecorView() + .setSystemUiVisibility( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + : View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } - public static void hideSoftKeyboard(@Nullable Activity activity) { - if (activity != null) { - View currentFocus = activity.getCurrentFocus(); - if (currentFocus != null) { - InputMethodManager inputMethodManager = (InputMethodManager) activity - .getSystemService(Activity.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) { - inputMethodManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); - } - } - } - } - - public static boolean isAllowedToDownloadMetadata(final @NonNull Context context) { - switch (PreferenceUtil.INSTANCE.getAutoDownloadImagesPolicy()) { - case "always": - return true; - case "only_wifi": - final ConnectivityManager connectivityManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); - return netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI && netInfo - .isConnectedOrConnecting(); - case "never": - default: - return false; - } - } - - public static boolean isLandscape() { - return App.Companion.getContext().getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - public static boolean isRTL(@NonNull Context context) { - Configuration config = context.getResources().getConfiguration(); - return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - } - - public static boolean isTablet() { - return App.Companion.getContext().getResources().getConfiguration().smallestScreenWidthDp >= 600; - } - - public static void openUrl(@NonNull Activity context, @NonNull String str) { - Intent intent = new Intent("android.intent.action.VIEW"); - intent.setData(Uri.parse(str)); - intent.setFlags(268435456); - context.startActivity(intent); - } - - public static void setAllowDrawUnderNavigationBar(Window window) { - window.setNavigationBarColor(Color.TRANSPARENT); - window.getDecorView().setSystemUiVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION : - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - ); - } - - public static void setAllowDrawUnderStatusBar(@NonNull Window window) { - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - } + public static void setAllowDrawUnderStatusBar(@NonNull Window window) { + window + .getDecorView() + .setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/RippleUtils.java b/app/src/main/java/code/name/monkey/retromusic/util/RippleUtils.java index 6a61c5dba..539166756 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/RippleUtils.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/RippleUtils.java @@ -5,142 +5,142 @@ import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Build; import android.util.StateSet; - import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; public class RippleUtils { - public static final boolean USE_FRAMEWORK_RIPPLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - private static final int[] PRESSED_STATE_SET = { - android.R.attr.state_pressed, - }; - private static final int[] HOVERED_FOCUSED_STATE_SET = { - android.R.attr.state_hovered, android.R.attr.state_focused, - }; - private static final int[] FOCUSED_STATE_SET = { - android.R.attr.state_focused, - }; - private static final int[] HOVERED_STATE_SET = { - android.R.attr.state_hovered, - }; + public static final boolean USE_FRAMEWORK_RIPPLE = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + private static final int[] PRESSED_STATE_SET = { + android.R.attr.state_pressed, + }; + private static final int[] HOVERED_FOCUSED_STATE_SET = { + android.R.attr.state_hovered, android.R.attr.state_focused, + }; + private static final int[] FOCUSED_STATE_SET = { + android.R.attr.state_focused, + }; + private static final int[] HOVERED_STATE_SET = { + android.R.attr.state_hovered, + }; - private static final int[] SELECTED_PRESSED_STATE_SET = { - android.R.attr.state_selected, android.R.attr.state_pressed, - }; - private static final int[] SELECTED_HOVERED_FOCUSED_STATE_SET = { - android.R.attr.state_selected, android.R.attr.state_hovered, android.R.attr.state_focused, - }; - private static final int[] SELECTED_FOCUSED_STATE_SET = { - android.R.attr.state_selected, android.R.attr.state_focused, - }; - private static final int[] SELECTED_HOVERED_STATE_SET = { - android.R.attr.state_selected, android.R.attr.state_hovered, - }; - private static final int[] SELECTED_STATE_SET = { - android.R.attr.state_selected, - }; + private static final int[] SELECTED_PRESSED_STATE_SET = { + android.R.attr.state_selected, android.R.attr.state_pressed, + }; + private static final int[] SELECTED_HOVERED_FOCUSED_STATE_SET = { + android.R.attr.state_selected, android.R.attr.state_hovered, android.R.attr.state_focused, + }; + private static final int[] SELECTED_FOCUSED_STATE_SET = { + android.R.attr.state_selected, android.R.attr.state_focused, + }; + private static final int[] SELECTED_HOVERED_STATE_SET = { + android.R.attr.state_selected, android.R.attr.state_hovered, + }; + private static final int[] SELECTED_STATE_SET = { + android.R.attr.state_selected, + }; - private static final int[] ENABLED_PRESSED_STATE_SET = { - android.R.attr.state_enabled, android.R.attr.state_pressed - }; + private static final int[] ENABLED_PRESSED_STATE_SET = { + android.R.attr.state_enabled, android.R.attr.state_pressed + }; - public static ColorStateList convertToRippleDrawableColor(@Nullable ColorStateList rippleColor) { - if (USE_FRAMEWORK_RIPPLE) { - int size = 2; + public static ColorStateList convertToRippleDrawableColor(@Nullable ColorStateList rippleColor) { + if (USE_FRAMEWORK_RIPPLE) { + int size = 2; - final int[][] states = new int[size][]; - final int[] colors = new int[size]; - int i = 0; + final int[][] states = new int[size][]; + final int[] colors = new int[size]; + int i = 0; - // Ideally we would define a different composite color for each state, but that causes the - // ripple animation to abort prematurely. - // So we only allow two base states: selected, and non-selected. For each base state, we only - // base the ripple composite on its pressed state. + // Ideally we would define a different composite color for each state, but that causes the + // ripple animation to abort prematurely. + // So we only allow two base states: selected, and non-selected. For each base state, we only + // base the ripple composite on its pressed state. - // Selected base state. - states[i] = SELECTED_STATE_SET; - colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET); - i++; + // Selected base state. + states[i] = SELECTED_STATE_SET; + colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET); + i++; - // Non-selected base state. - states[i] = StateSet.NOTHING; - colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET); - i++; + // Non-selected base state. + states[i] = StateSet.NOTHING; + colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET); + i++; - return new ColorStateList(states, colors); - } else { - int size = 10; + return new ColorStateList(states, colors); + } else { + int size = 10; - final int[][] states = new int[size][]; - final int[] colors = new int[size]; - int i = 0; + final int[][] states = new int[size][]; + final int[] colors = new int[size]; + int i = 0; - states[i] = SELECTED_PRESSED_STATE_SET; - colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET); - i++; + states[i] = SELECTED_PRESSED_STATE_SET; + colors[i] = getColorForState(rippleColor, SELECTED_PRESSED_STATE_SET); + i++; - states[i] = SELECTED_HOVERED_FOCUSED_STATE_SET; - colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_FOCUSED_STATE_SET); - i++; + states[i] = SELECTED_HOVERED_FOCUSED_STATE_SET; + colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_FOCUSED_STATE_SET); + i++; - states[i] = SELECTED_FOCUSED_STATE_SET; - colors[i] = getColorForState(rippleColor, SELECTED_FOCUSED_STATE_SET); - i++; + states[i] = SELECTED_FOCUSED_STATE_SET; + colors[i] = getColorForState(rippleColor, SELECTED_FOCUSED_STATE_SET); + i++; - states[i] = SELECTED_HOVERED_STATE_SET; - colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_STATE_SET); - i++; + states[i] = SELECTED_HOVERED_STATE_SET; + colors[i] = getColorForState(rippleColor, SELECTED_HOVERED_STATE_SET); + i++; - // Checked state. - states[i] = SELECTED_STATE_SET; - colors[i] = Color.TRANSPARENT; - i++; + // Checked state. + states[i] = SELECTED_STATE_SET; + colors[i] = Color.TRANSPARENT; + i++; - states[i] = PRESSED_STATE_SET; - colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET); - i++; + states[i] = PRESSED_STATE_SET; + colors[i] = getColorForState(rippleColor, PRESSED_STATE_SET); + i++; - states[i] = HOVERED_FOCUSED_STATE_SET; - colors[i] = getColorForState(rippleColor, HOVERED_FOCUSED_STATE_SET); - i++; + states[i] = HOVERED_FOCUSED_STATE_SET; + colors[i] = getColorForState(rippleColor, HOVERED_FOCUSED_STATE_SET); + i++; - states[i] = FOCUSED_STATE_SET; - colors[i] = getColorForState(rippleColor, FOCUSED_STATE_SET); - i++; + states[i] = FOCUSED_STATE_SET; + colors[i] = getColorForState(rippleColor, FOCUSED_STATE_SET); + i++; - states[i] = HOVERED_STATE_SET; - colors[i] = getColorForState(rippleColor, HOVERED_STATE_SET); - i++; + states[i] = HOVERED_STATE_SET; + colors[i] = getColorForState(rippleColor, HOVERED_STATE_SET); + i++; - // Default state. - states[i] = StateSet.NOTHING; - colors[i] = Color.TRANSPARENT; - i++; + // Default state. + states[i] = StateSet.NOTHING; + colors[i] = Color.TRANSPARENT; + i++; - return new ColorStateList(states, colors); - } + return new ColorStateList(states, colors); } + } - @ColorInt - private static int getColorForState(@Nullable ColorStateList rippleColor, int[] state) { - int color; - if (rippleColor != null) { - color = rippleColor.getColorForState(state, rippleColor.getDefaultColor()); - } else { - color = Color.TRANSPARENT; - } - return USE_FRAMEWORK_RIPPLE ? doubleAlpha(color) : color; + @ColorInt + private static int getColorForState(@Nullable ColorStateList rippleColor, int[] state) { + int color; + if (rippleColor != null) { + color = rippleColor.getColorForState(state, rippleColor.getDefaultColor()); + } else { + color = Color.TRANSPARENT; } + return USE_FRAMEWORK_RIPPLE ? doubleAlpha(color) : color; + } - /** - * On API 21+, the framework composites a ripple color onto the display at about 50% opacity. - * Since we are providing precise ripple colors, cancel that out by doubling the opacity here. - */ - @ColorInt - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static int doubleAlpha(@ColorInt int color) { - int alpha = Math.min(2 * Color.alpha(color), 255); - return ColorUtils.setAlphaComponent(color, alpha); - } + /** + * On API 21+, the framework composites a ripple color onto the display at about 50% opacity. + * Since we are providing precise ripple colors, cancel that out by doubling the opacity here. + */ + @ColorInt + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static int doubleAlpha(@ColorInt int color) { + int alpha = Math.min(2 * Color.alpha(color), 255); + return ColorUtils.setAlphaComponent(color, alpha); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java index 10da6d035..64150ed0e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/SAFUtil.java @@ -26,278 +26,283 @@ import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; - import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.Fragment; - -import org.jaudiotagger.audio.AudioFile; -import org.jaudiotagger.audio.exceptions.CannotWriteException; -import org.jaudiotagger.audio.generic.Utils; - +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.model.Song; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.model.Song; +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.exceptions.CannotWriteException; +import org.jaudiotagger.audio.generic.Utils; public class SAFUtil { - public static final String TAG = SAFUtil.class.getSimpleName(); - public static final String SEPARATOR = "###/SAF/###"; + public static final String TAG = SAFUtil.class.getSimpleName(); + public static final String SEPARATOR = "###/SAF/###"; - public static final int REQUEST_SAF_PICK_FILE = 42; - public static final int REQUEST_SAF_PICK_TREE = 43; + public static final int REQUEST_SAF_PICK_FILE = 42; + public static final int REQUEST_SAF_PICK_TREE = 43; - public static boolean isSAFRequired(File file) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !file.canWrite(); + public static boolean isSAFRequired(File file) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !file.canWrite(); + } + + public static boolean isSAFRequired(String path) { + return isSAFRequired(new File(path)); + } + + public static boolean isSAFRequired(AudioFile audio) { + return isSAFRequired(audio.getFile()); + } + + public static boolean isSAFRequired(Song song) { + return isSAFRequired(song.getData()); + } + + public static boolean isSAFRequired(List paths) { + for (String path : paths) { + if (isSAFRequired(path)) return true; + } + return false; + } + + public static boolean isSAFRequiredForSongs(List songs) { + for (Song song : songs) { + if (isSAFRequired(song)) return true; + } + return false; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openFilePicker(Activity activity) { + Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("audio/*"); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openFilePicker(Fragment fragment) { + Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("audio/*"); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void openTreePicker(Activity activity) { + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static void openTreePicker(Fragment fragment) { + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + i.putExtra("android.content.extra.SHOW_ADVANCED", true); + fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void saveTreeUri(Context context, Intent data) { + Uri uri = data.getData(); + context + .getContentResolver() + .takePersistableUriPermission( + uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + PreferenceUtil.INSTANCE.setSafSdCardUri(uri.toString()); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean isTreeUriSaved(Context context) { + return !TextUtils.isEmpty(PreferenceUtil.INSTANCE.getSafSdCardUri()); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean isSDCardAccessGranted(Context context) { + if (!isTreeUriSaved(context)) return false; + + String sdcardUri = PreferenceUtil.INSTANCE.getSafSdCardUri(); + + List perms = context.getContentResolver().getPersistedUriPermissions(); + for (UriPermission perm : perms) { + if (perm.getUri().toString().equals(sdcardUri) && perm.isWritePermission()) return true; } - public static boolean isSAFRequired(String path) { - return isSAFRequired(new File(path)); + return false; + } + + /** + * https://github.com/vanilla-music/vanilla-music-tag-editor/commit/e00e87fef289f463b6682674aa54be834179ccf0#diff-d436417358d5dfbb06846746d43c47a5R359 + * Finds needed file through Document API for SAF. It's not optimized yet - you can still gain + * wrong URI on files such as "/a/b/c.mp3" and "/b/a/c.mp3", but I consider it complete enough to + * be usable. + * + * @param dir - document file representing current dir of search + * @param segments - path segments that are left to find + * @return URI for found file. Null if nothing found. + */ + @Nullable + public static Uri findDocument(DocumentFile dir, List segments) { + for (DocumentFile file : dir.listFiles()) { + int index = segments.indexOf(file.getName()); + if (index == -1) { + continue; + } + + if (file.isDirectory()) { + segments.remove(file.getName()); + return findDocument(file, segments); + } + + if (file.isFile() && index == segments.size() - 1) { + // got to the last part + return file.getUri(); + } } - public static boolean isSAFRequired(AudioFile audio) { - return isSAFRequired(audio.getFile()); + return null; + } + + public static void write(Context context, AudioFile audio, Uri safUri) { + if (isSAFRequired(audio)) { + writeSAF(context, audio, safUri); + } else { + try { + writeFile(audio); + } catch (CannotWriteException e) { + e.printStackTrace(); + } + } + } + + public static void writeFile(AudioFile audio) throws CannotWriteException { + audio.commit(); + } + + public static void writeSAF(Context context, AudioFile audio, Uri safUri) { + Uri uri = null; + + if (context == null) { + Log.e(TAG, "writeSAF: context == null"); + return; } - public static boolean isSAFRequired(Song song) { - return isSAFRequired(song.getData()); + if (isTreeUriSaved(context)) { + List pathSegments = + new ArrayList<>(Arrays.asList(audio.getFile().getAbsolutePath().split("/"))); + Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri()); + uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); } - public static boolean isSAFRequired(List paths) { - for (String path : paths) { - if (isSAFRequired(path)) return true; - } - return false; + if (uri == null) { + uri = safUri; } - public static boolean isSAFRequiredForSongs(List songs) { - for (Song song : songs) { - if (isSAFRequired(song)) return true; - } - return false; + if (uri == null) { + Log.e(TAG, "writeSAF: Can't get SAF URI"); + toast(context, context.getString(R.string.saf_error_uri)); + return; } - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openFilePicker(Activity activity) { - Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("audio/*"); - i.putExtra("android.content.extra.SHOW_ADVANCED", true); - activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + try { + // copy file to app folder to use jaudiotagger + final File original = audio.getFile(); + File temp = File.createTempFile("tmp-media", '.' + Utils.getExtension(original)); + Utils.copy(original, temp); + temp.deleteOnExit(); + audio.setFile(temp); + writeFile(audio); + + ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "rw"); + if (pfd == null) { + Log.e(TAG, "writeSAF: SAF provided incorrect URI: " + uri); + return; + } + + // now read persisted data and write it to real FD provided by SAF + FileInputStream fis = new FileInputStream(temp); + byte[] audioContent = FileUtil.readBytes(fis); + FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor()); + fos.write(audioContent); + fos.close(); + + temp.delete(); + } catch (final Exception e) { + Log.e(TAG, "writeSAF: Failed to write to file descriptor provided by SAF", e); + + toast( + context, + String.format(context.getString(R.string.saf_write_failed), e.getLocalizedMessage())); + } + } + + public static void delete(Context context, String path, Uri safUri) { + if (isSAFRequired(path)) { + deleteSAF(context, path, safUri); + } else { + try { + deleteFile(path); + } catch (NullPointerException e) { + Log.e("MusicUtils", "Failed to find file " + path); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void deleteFile(String path) { + new File(path).delete(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void deleteSAF(Context context, String path, Uri safUri) { + Uri uri = null; + + if (context == null) { + Log.e(TAG, "deleteSAF: context == null"); + return; } - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openFilePicker(Fragment fragment) { - Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("audio/*"); - i.putExtra("android.content.extra.SHOW_ADVANCED", true); - fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_FILE); + if (isTreeUriSaved(context)) { + List pathSegments = new ArrayList<>(Arrays.asList(path.split("/"))); + Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri()); + uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static void openTreePicker(Activity activity) { - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.putExtra("android.content.extra.SHOW_ADVANCED", true); - activity.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + if (uri == null) { + uri = safUri; } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static void openTreePicker(Fragment fragment) { - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.putExtra("android.content.extra.SHOW_ADVANCED", true); - fragment.startActivityForResult(i, SAFUtil.REQUEST_SAF_PICK_TREE); + if (uri == null) { + Log.e(TAG, "deleteSAF: Can't get SAF URI"); + toast(context, context.getString(R.string.saf_error_uri)); + return; } - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void saveTreeUri(Context context, Intent data) { - Uri uri = data.getData(); - context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - PreferenceUtil.INSTANCE.setSafSdCardUri(uri.toString()); + try { + DocumentsContract.deleteDocument(context.getContentResolver(), uri); + } catch (final Exception e) { + Log.e(TAG, "deleteSAF: Failed to delete a file descriptor provided by SAF", e); + + toast( + context, + String.format(context.getString(R.string.saf_delete_failed), e.getLocalizedMessage())); } + } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static boolean isTreeUriSaved(Context context) { - return !TextUtils.isEmpty(PreferenceUtil.INSTANCE.getSafSdCardUri()); + private static void toast(final Context context, final String message) { + if (context instanceof Activity) { + ((Activity) context) + .runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static boolean isSDCardAccessGranted(Context context) { - if (!isTreeUriSaved(context)) return false; - - String sdcardUri = PreferenceUtil.INSTANCE.getSafSdCardUri(); - - List perms = context.getContentResolver().getPersistedUriPermissions(); - for (UriPermission perm : perms) { - if (perm.getUri().toString().equals(sdcardUri) && perm.isWritePermission()) return true; - } - - return false; - } - - /** - * https://github.com/vanilla-music/vanilla-music-tag-editor/commit/e00e87fef289f463b6682674aa54be834179ccf0#diff-d436417358d5dfbb06846746d43c47a5R359 - * Finds needed file through Document API for SAF. It's not optimized yet - you can still gain wrong URI on - * files such as "/a/b/c.mp3" and "/b/a/c.mp3", but I consider it complete enough to be usable. - * - * @param dir - document file representing current dir of search - * @param segments - path segments that are left to find - * @return URI for found file. Null if nothing found. - */ - @Nullable - public static Uri findDocument(DocumentFile dir, List segments) { - for (DocumentFile file : dir.listFiles()) { - int index = segments.indexOf(file.getName()); - if (index == -1) { - continue; - } - - if (file.isDirectory()) { - segments.remove(file.getName()); - return findDocument(file, segments); - } - - if (file.isFile() && index == segments.size() - 1) { - // got to the last part - return file.getUri(); - } - } - - return null; - } - - public static void write(Context context, AudioFile audio, Uri safUri) { - if (isSAFRequired(audio)) { - writeSAF(context, audio, safUri); - } else { - try { - writeFile(audio); - } catch (CannotWriteException e) { - e.printStackTrace(); - } - } - } - - public static void writeFile(AudioFile audio) throws CannotWriteException { - audio.commit(); - } - - public static void writeSAF(Context context, AudioFile audio, Uri safUri) { - Uri uri = null; - - if (context == null) { - Log.e(TAG, "writeSAF: context == null"); - return; - } - - if (isTreeUriSaved(context)) { - List pathSegments = new ArrayList<>(Arrays.asList(audio.getFile().getAbsolutePath().split("/"))); - Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri()); - uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); - } - - if (uri == null) { - uri = safUri; - } - - if (uri == null) { - Log.e(TAG, "writeSAF: Can't get SAF URI"); - toast(context, context.getString(R.string.saf_error_uri)); - return; - } - - try { - // copy file to app folder to use jaudiotagger - final File original = audio.getFile(); - File temp = File.createTempFile("tmp-media", '.' + Utils.getExtension(original)); - Utils.copy(original, temp); - temp.deleteOnExit(); - audio.setFile(temp); - writeFile(audio); - - ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "rw"); - if (pfd == null) { - Log.e(TAG, "writeSAF: SAF provided incorrect URI: " + uri); - return; - } - - // now read persisted data and write it to real FD provided by SAF - FileInputStream fis = new FileInputStream(temp); - byte[] audioContent = FileUtil.readBytes(fis); - FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor()); - fos.write(audioContent); - fos.close(); - - temp.delete(); - } catch (final Exception e) { - Log.e(TAG, "writeSAF: Failed to write to file descriptor provided by SAF", e); - - toast(context, String.format(context.getString(R.string.saf_write_failed), e.getLocalizedMessage())); - } - } - - public static void delete(Context context, String path, Uri safUri) { - if (isSAFRequired(path)) { - deleteSAF(context, path, safUri); - } else { - try { - deleteFile(path); - } catch (NullPointerException e) { - Log.e("MusicUtils", "Failed to find file " + path); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void deleteFile(String path) { - new File(path).delete(); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void deleteSAF(Context context, String path, Uri safUri) { - Uri uri = null; - - if (context == null) { - Log.e(TAG, "deleteSAF: context == null"); - return; - } - - if (isTreeUriSaved(context)) { - List pathSegments = new ArrayList<>(Arrays.asList(path.split("/"))); - Uri sdcard = Uri.parse(PreferenceUtil.INSTANCE.getSafSdCardUri()); - uri = findDocument(DocumentFile.fromTreeUri(context, sdcard), pathSegments); - } - - if (uri == null) { - uri = safUri; - } - - if (uri == null) { - Log.e(TAG, "deleteSAF: Can't get SAF URI"); - toast(context, context.getString(R.string.saf_error_uri)); - return; - } - - try { - DocumentsContract.deleteDocument(context.getContentResolver(), uri); - } catch (final Exception e) { - Log.e(TAG, "deleteSAF: Failed to delete a file descriptor provided by SAF", e); - - toast(context, String.format(context.getString(R.string.saf_delete_failed), e.getLocalizedMessage())); - } - } - - private static void toast(final Context context, final String message) { - if (context instanceof Activity) { - ((Activity) context).runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); - } - } - + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/SwipeAndDragHelper.java b/app/src/main/java/code/name/monkey/retromusic/util/SwipeAndDragHelper.java index 1c82c7aa7..2ab25e189 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/SwipeAndDragHelper.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/SwipeAndDragHelper.java @@ -15,58 +15,58 @@ package code.name.monkey.retromusic.util; import android.graphics.Canvas; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; public class SwipeAndDragHelper extends ItemTouchHelper.Callback { - private ActionCompletionContract contract; + private ActionCompletionContract contract; - public SwipeAndDragHelper(@NonNull ActionCompletionContract contract) { - this.contract = contract; - } - - @Override - public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; - return makeMovementFlags(dragFlags, 0); - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - contract.onViewMoved(viewHolder.getLayoutPosition(), target.getLayoutPosition()); - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - } - - @Override - public boolean isLongPressDragEnabled() { - return false; - } - - @Override - public void onChildDraw(Canvas c, - RecyclerView recyclerView, - RecyclerView.ViewHolder viewHolder, - float dX, - float dY, - int actionState, - boolean isCurrentlyActive) { - if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { - float alpha = 1 - (Math.abs(dX) / recyclerView.getWidth()); - viewHolder.itemView.setAlpha(alpha); - } - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - } - - public interface ActionCompletionContract { - void onViewMoved(int oldPosition, int newPosition); + public SwipeAndDragHelper(@NonNull ActionCompletionContract contract) { + this.contract = contract; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + return makeMovementFlags(dragFlags, 0); + } + + @Override + public boolean onMove( + RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + RecyclerView.ViewHolder target) { + contract.onViewMoved(viewHolder.getLayoutPosition(), target.getLayoutPosition()); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {} + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public void onChildDraw( + Canvas c, + RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, + float dX, + float dY, + int actionState, + boolean isCurrentlyActive) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + float alpha = 1 - (Math.abs(dX) / recyclerView.getWidth()); + viewHolder.itemView.setAlpha(alpha); } + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + public interface ActionCompletionContract { + void onViewMoved(int oldPosition, int newPosition); + } } - diff --git a/app/src/main/java/code/name/monkey/retromusic/util/TempUtils.java b/app/src/main/java/code/name/monkey/retromusic/util/TempUtils.java index 2a0fac4e7..1f5f1e2b6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/TempUtils.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/TempUtils.java @@ -14,69 +14,67 @@ package code.name.monkey.retromusic.util; -/** - * @author Hemanth S (h4h13). - */ +/** @author Hemanth S (h4h13). */ public class TempUtils { - // Enums - public static final int TEMPO_STROLL = 0; - public static final int TEMPO_WALK = 1; - public static final int TEMPO_LIGHT_JOG = 2; - public static final int TEMPO_JOG = 3; - public static final int TEMPO_RUN = 4; - public static final int TEMPO_SPRINT = 5; - public static final int TEMPO_UNKNOWN = 6; + // Enums + public static final int TEMPO_STROLL = 0; + public static final int TEMPO_WALK = 1; + public static final int TEMPO_LIGHT_JOG = 2; + public static final int TEMPO_JOG = 3; + public static final int TEMPO_RUN = 4; + public static final int TEMPO_SPRINT = 5; + public static final int TEMPO_UNKNOWN = 6; - // take BPM as an int - public static int getTempoFromBPM(int bpm) { + // take BPM as an int + public static int getTempoFromBPM(int bpm) { - // STROLL less than 60 - if (bpm < 60) { - return TEMPO_STROLL; - } - - // WALK between 60 and 70, or between 120 and 140 - else if (bpm < 70 || bpm >= 120 && bpm < 140) { - return TEMPO_WALK; - } - - // LIGHT_JOG between 70 and 80, or between 140 and 160 - else if (bpm < 80 || bpm >= 140 && bpm < 160) { - return TEMPO_LIGHT_JOG; - } - - // JOG between 80 and 90, or between 160 and 180 - else if (bpm < 90 || bpm >= 160 && bpm < 180) { - return TEMPO_JOG; - } - - // RUN between 90 and 100, or between 180 and 200 - else if (bpm < 100 || bpm >= 180 && bpm < 200) { - return TEMPO_RUN; - } - - // SPRINT between 100 and 120 - else if (bpm < 120) { - return TEMPO_SPRINT; - } - - // UNKNOWN - else { - return TEMPO_UNKNOWN; - } + // STROLL less than 60 + if (bpm < 60) { + return TEMPO_STROLL; } - // take BPM as a string - public static int getTempoFromBPM(String bpm) { - // cast to an int from string - try { - // convert the string to an int - return getTempoFromBPM(Integer.parseInt(bpm.trim())); - } catch (NumberFormatException nfe) { - - // - return TEMPO_UNKNOWN; - } + // WALK between 60 and 70, or between 120 and 140 + else if (bpm < 70 || bpm >= 120 && bpm < 140) { + return TEMPO_WALK; } + + // LIGHT_JOG between 70 and 80, or between 140 and 160 + else if (bpm < 80 || bpm >= 140 && bpm < 160) { + return TEMPO_LIGHT_JOG; + } + + // JOG between 80 and 90, or between 160 and 180 + else if (bpm < 90 || bpm >= 160 && bpm < 180) { + return TEMPO_JOG; + } + + // RUN between 90 and 100, or between 180 and 200 + else if (bpm < 100 || bpm >= 180 && bpm < 200) { + return TEMPO_RUN; + } + + // SPRINT between 100 and 120 + else if (bpm < 120) { + return TEMPO_SPRINT; + } + + // UNKNOWN + else { + return TEMPO_UNKNOWN; + } + } + + // take BPM as a string + public static int getTempoFromBPM(String bpm) { + // cast to an int from string + try { + // convert the string to an int + return getTempoFromBPM(Integer.parseInt(bpm.trim())); + } catch (NumberFormatException nfe) { + + // + return TEMPO_UNKNOWN; + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java b/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java index b22bab5df..53dfc3e5d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/ImageUtils.java @@ -30,118 +30,111 @@ import android.graphics.drawable.Drawable; * @hide */ public class ImageUtils { - // Amount (max is 255) that two channels can differ before the color is no longer "gray". - private static final int TOLERANCE = 20; - // Alpha amount for which values below are considered transparent. - private static final int ALPHA_TOLERANCE = 50; - // Size of the smaller bitmap we're actually going to scan. - private static final int COMPACT_BITMAP_SIZE = 64; // pixels - private final Matrix mTempMatrix = new Matrix(); - private int[] mTempBuffer; - private Bitmap mTempCompactBitmap; - private Canvas mTempCompactBitmapCanvas; - private Paint mTempCompactBitmapPaint; + // Amount (max is 255) that two channels can differ before the color is no longer "gray". + private static final int TOLERANCE = 20; + // Alpha amount for which values below are considered transparent. + private static final int ALPHA_TOLERANCE = 50; + // Size of the smaller bitmap we're actually going to scan. + private static final int COMPACT_BITMAP_SIZE = 64; // pixels + private final Matrix mTempMatrix = new Matrix(); + private int[] mTempBuffer; + private Bitmap mTempCompactBitmap; + private Canvas mTempCompactBitmapCanvas; + private Paint mTempCompactBitmapPaint; - /** - * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect - * gray"; if all three channels are approximately equal, this will return true. - *

- * Note that really transparent colors are always grayscale. - */ - public static boolean isGrayscale(int color) { - int alpha = 0xFF & (color >> 24); - if (alpha < ALPHA_TOLERANCE) { - return true; - } - int r = 0xFF & (color >> 16); - int g = 0xFF & (color >> 8); - int b = 0xFF & color; - return Math.abs(r - g) < TOLERANCE - && Math.abs(r - b) < TOLERANCE - && Math.abs(g - b) < TOLERANCE; + /** + * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect gray"; if + * all three channels are approximately equal, this will return true. + * + *

Note that really transparent colors are always grayscale. + */ + public static boolean isGrayscale(int color) { + int alpha = 0xFF & (color >> 24); + if (alpha < ALPHA_TOLERANCE) { + return true; } + int r = 0xFF & (color >> 16); + int g = 0xFF & (color >> 8); + int b = 0xFF & color; + return Math.abs(r - g) < TOLERANCE + && Math.abs(r - b) < TOLERANCE + && Math.abs(g - b) < TOLERANCE; + } - /** - * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. - */ - public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, - int maxHeight) { - if (drawable == null) { - return null; - } - int originalWidth = drawable.getIntrinsicWidth(); - int originalHeight = drawable.getIntrinsicHeight(); - if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) && - (drawable instanceof BitmapDrawable)) { - return ((BitmapDrawable) drawable).getBitmap(); - } - if (originalHeight <= 0 || originalWidth <= 0) { - return null; - } - // create a new bitmap, scaling down to fit the max dimensions of - // a large notification icon if necessary - float ratio = Math.min((float) maxWidth / (float) originalWidth, - (float) maxHeight / (float) originalHeight); - ratio = Math.min(1.0f, ratio); - int scaledWidth = (int) (ratio * originalWidth); - int scaledHeight = (int) (ratio * originalHeight); - Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); - // and paint our app bitmap on it - Canvas canvas = new Canvas(result); - drawable.setBounds(0, 0, scaledWidth, scaledHeight); - drawable.draw(canvas); - return result; + /** Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. */ + public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, int maxHeight) { + if (drawable == null) { + return null; } - - /** - * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect - * gray". - *

- * Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than - * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements - * will survive the squeezing process, contaminating the result with color. - */ - public boolean isGrayscale(Bitmap bitmap) { - int height = bitmap.getHeight(); - int width = bitmap.getWidth(); - - // shrink to a more manageable (yet hopefully no more or less colorful) size - if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) { - if (mTempCompactBitmap == null) { - mTempCompactBitmap = Bitmap.createBitmap( - COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Config.ARGB_8888 - ); - mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap); - mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mTempCompactBitmapPaint.setFilterBitmap(true); - } - mTempMatrix.reset(); - mTempMatrix.setScale( - (float) COMPACT_BITMAP_SIZE / width, - (float) COMPACT_BITMAP_SIZE / height, - 0, 0); - mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase - mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint); - bitmap = mTempCompactBitmap; - width = height = COMPACT_BITMAP_SIZE; - } - final int size = height * width; - ensureBufferSize(size); - bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); - for (int i = 0; i < size; i++) { - if (!isGrayscale(mTempBuffer[i])) { - return false; - } - } - return true; + int originalWidth = drawable.getIntrinsicWidth(); + int originalHeight = drawable.getIntrinsicHeight(); + if ((originalWidth <= maxWidth) + && (originalHeight <= maxHeight) + && (drawable instanceof BitmapDrawable)) { + return ((BitmapDrawable) drawable).getBitmap(); } - - /** - * Makes sure that {@code mTempBuffer} has at least length {@code size}. - */ - private void ensureBufferSize(int size) { - if (mTempBuffer == null || mTempBuffer.length < size) { - mTempBuffer = new int[size]; - } + if (originalHeight <= 0 || originalWidth <= 0) { + return null; } -} \ No newline at end of file + // create a new bitmap, scaling down to fit the max dimensions of + // a large notification icon if necessary + float ratio = + Math.min( + (float) maxWidth / (float) originalWidth, (float) maxHeight / (float) originalHeight); + ratio = Math.min(1.0f, ratio); + int scaledWidth = (int) (ratio * originalWidth); + int scaledHeight = (int) (ratio * originalHeight); + Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); + // and paint our app bitmap on it + Canvas canvas = new Canvas(result); + drawable.setBounds(0, 0, scaledWidth, scaledHeight); + drawable.draw(canvas); + return result; + } + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect gray". + * + *

Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than + * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements will + * survive the squeezing process, contaminating the result with color. + */ + public boolean isGrayscale(Bitmap bitmap) { + int height = bitmap.getHeight(); + int width = bitmap.getWidth(); + + // shrink to a more manageable (yet hopefully no more or less colorful) size + if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) { + if (mTempCompactBitmap == null) { + mTempCompactBitmap = + Bitmap.createBitmap(COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Config.ARGB_8888); + mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap); + mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTempCompactBitmapPaint.setFilterBitmap(true); + } + mTempMatrix.reset(); + mTempMatrix.setScale( + (float) COMPACT_BITMAP_SIZE / width, (float) COMPACT_BITMAP_SIZE / height, 0, 0); + mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase + mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint); + bitmap = mTempCompactBitmap; + width = height = COMPACT_BITMAP_SIZE; + } + final int size = height * width; + ensureBufferSize(size); + bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); + for (int i = 0; i < size; i++) { + if (!isGrayscale(mTempBuffer[i])) { + return false; + } + } + return true; + } + + /** Makes sure that {@code mTempBuffer} has at least length {@code size}. */ + private void ensureBufferSize(int size) { + if (mTempBuffer == null || mTempBuffer.length < size) { + mTempBuffer = new int[size]; + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java b/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java index cb8482c5b..2e1cb4b1c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/MediaNotificationProcessor.java @@ -16,6 +16,8 @@ package code.name.monkey.retromusic.util.color; +import static androidx.core.graphics.ColorUtils.RGBToXYZ; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -23,485 +25,472 @@ import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; - import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.palette.graphics.Palette; - -import java.util.List; - import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ColorUtil; import code.name.monkey.retromusic.R; +import java.util.List; -import static androidx.core.graphics.ColorUtils.RGBToXYZ; - -/** - * A class the processes media notifications and extracts the right text and background colors. - */ +/** A class the processes media notifications and extracts the right text and background colors. */ public class MediaNotificationProcessor { - /** - * The fraction below which we select the vibrant instead of the light/dark vibrant color - */ - private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f; + /** The fraction below which we select the vibrant instead of the light/dark vibrant color */ + private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f; - /** - * Minimum saturation that a muted color must have if there exists if deciding between two - * colors - */ - private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f; + /** + * Minimum saturation that a muted color must have if there exists if deciding between two colors + */ + private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f; - /** - * Minimum fraction that any color must have to be picked up as a text color - */ - private static final double MINIMUM_IMAGE_FRACTION = 0.002; + /** Minimum fraction that any color must have to be picked up as a text color */ + private static final double MINIMUM_IMAGE_FRACTION = 0.002; - /** - * The population fraction to select the dominant color as the text color over a the colored - * ones. - */ - private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f; + /** + * The population fraction to select the dominant color as the text color over a the colored ones. + */ + private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f; - /** - * The population fraction to select a white or black color as the background over a color. - */ - private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f; - private static final float BLACK_MAX_LIGHTNESS = 0.08f; - private static final float WHITE_MIN_LIGHTNESS = 0.90f; - private static final int RESIZE_BITMAP_AREA = 150 * 150; - private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); - /** - * The lightness difference that has to be added to the primary text color to obtain the - * secondary text color when the background is light. - */ - private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20; - /** - * The lightness difference that has to be added to the primary text color to obtain the - * secondary text color when the background is dark. - * A bit less then the above value, since it looks better on dark backgrounds. - */ - private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10; - private static final String TAG = "ColorPicking"; - private float[] mFilteredBackgroundHsl = null; - private Palette.Filter mBlackWhiteFilter = new Palette.Filter() { + /** The population fraction to select a white or black color as the background over a color. */ + private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f; + + private static final float BLACK_MAX_LIGHTNESS = 0.08f; + private static final float WHITE_MIN_LIGHTNESS = 0.90f; + private static final int RESIZE_BITMAP_AREA = 150 * 150; + private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); + /** + * The lightness difference that has to be added to the primary text color to obtain the secondary + * text color when the background is light. + */ + private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20; + /** + * The lightness difference that has to be added to the primary text color to obtain the secondary + * text color when the background is dark. A bit less then the above value, since it looks better + * on dark backgrounds. + */ + private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10; + + private static final String TAG = "ColorPicking"; + private float[] mFilteredBackgroundHsl = null; + private Palette.Filter mBlackWhiteFilter = + new Palette.Filter() { @Override public boolean isAllowed(int rgb, @NonNull float[] hsl) { - return !isWhiteOrBlack(hsl); + return !isWhiteOrBlack(hsl); } - }; - private boolean mIsLowPriority; - private int backgroundColor; - private int secondaryTextColor; - private int primaryTextColor; - private int actionBarColor; - private Drawable drawable; - private Context context; + }; + private boolean mIsLowPriority; + private int backgroundColor; + private int secondaryTextColor; + private int primaryTextColor; + private int actionBarColor; + private Drawable drawable; + private Context context; - public MediaNotificationProcessor(Context context, Drawable drawable) { - this.context = context; - this.drawable = drawable; - getMediaPalette(); + public MediaNotificationProcessor(Context context, Drawable drawable) { + this.context = context; + this.drawable = drawable; + getMediaPalette(); + } + + public MediaNotificationProcessor(Context context, Bitmap bitmap) { + this.context = context; + this.drawable = new BitmapDrawable(context.getResources(), bitmap); + getMediaPalette(); + } + + public MediaNotificationProcessor(Context context) { + this.context = context; + } + + private static boolean isColorLight(int backgroundColor) { + return calculateLuminance(backgroundColor) > 0.5f; + } + + /** + * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. + * + *

Defined as the Y component in the XYZ representation of {@code color}. + */ + @FloatRange(from = 0.0, to = 1.0) + private static double calculateLuminance(@ColorInt int color) { + final double[] result = getTempDouble3Array(); + colorToXYZ(color, result); + // Luminance is the Y component + return result[1] / 100; + } + + private static double[] getTempDouble3Array() { + double[] result = TEMP_ARRAY.get(); + if (result == null) { + result = new double[3]; + TEMP_ARRAY.set(result); } + return result; + } + private static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { + RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); + } - public MediaNotificationProcessor(Context context, Bitmap bitmap) { - this.context = context; - this.drawable = new BitmapDrawable(context.getResources(), bitmap); - getMediaPalette(); - } - - public MediaNotificationProcessor(Context context) { - this.context = context; - } - - private static boolean isColorLight(int backgroundColor) { - return calculateLuminance(backgroundColor) > 0.5f; - } - - /** - * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. - *

Defined as the Y component in the XYZ representation of {@code color}.

- */ - @FloatRange(from = 0.0, to = 1.0) - private static double calculateLuminance(@ColorInt int color) { - final double[] result = getTempDouble3Array(); - colorToXYZ(color, result); - // Luminance is the Y component - return result[1] / 100; - } - - private static double[] getTempDouble3Array() { - double[] result = TEMP_ARRAY.get(); - if (result == null) { - result = new double[3]; - TEMP_ARRAY.set(result); - } - return result; - } - - private static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { - RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); - } - - public void getPaletteAsync(final OnPaletteLoadedListener onPaletteLoadedListener, Drawable drawable) { - this.drawable = drawable; - final Handler handler = new Handler(); - new Thread(new Runnable() { - @Override - public void run() { + public void getPaletteAsync( + final OnPaletteLoadedListener onPaletteLoadedListener, Drawable drawable) { + this.drawable = drawable; + final Handler handler = new Handler(); + new Thread( + new Runnable() { + @Override + public void run() { getMediaPalette(); - handler.post(new Runnable() { - @Override - public void run() { + handler.post( + new Runnable() { + @Override + public void run() { onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this); - } - }); - } - }).start(); - - } - - public void getPaletteAsync(OnPaletteLoadedListener onPaletteLoadedListener, Bitmap bitmap) { - this.drawable = new BitmapDrawable(context.getResources(), bitmap); - getPaletteAsync(onPaletteLoadedListener, this.drawable); - } - - /** - * Processes a drawable and calculates the appropriate colors that should - * be used. - */ - private void getMediaPalette() { - Bitmap bitmap; - if (drawable != null) { - // We're transforming the builder, let's make sure all baked in RemoteViews are - // rebuilt! - - int width = drawable.getIntrinsicWidth(); - int height = drawable.getIntrinsicHeight(); - int area = width * height; - if (area > RESIZE_BITMAP_AREA) { - double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); - width = (int) (factor * width); - height = (int) (factor * height); - - bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, width, height); - drawable.draw(canvas); - - // for the background we only take the left side of the image to ensure - // a smooth transition - Palette.Builder paletteBuilder = Palette.from(bitmap) - .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) - .clearFilters() // we want all colors, red / white / black ones too! - .resizeBitmapArea(RESIZE_BITMAP_AREA); - Palette palette = paletteBuilder.generate(); - backgroundColor = findBackgroundColorAndFilter(drawable); - // we want most of the full region again, slightly shifted to the right - float textColorStartWidthFraction = 0.4f; - paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, - bitmap.getWidth(), - bitmap.getHeight()); - if (mFilteredBackgroundHsl != null) { - paletteBuilder.addFilter(new Palette.Filter() { - @Override - public boolean isAllowed(int rgb, @NonNull float[] hsl) { - // at least 10 degrees hue difference - float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); - return diff > 10 && diff < 350; - } + } }); - } - paletteBuilder.addFilter(mBlackWhiteFilter); - palette = paletteBuilder.generate(); - int foregroundColor = selectForegroundColor(backgroundColor, palette); - ensureColors(backgroundColor, foregroundColor); - } - } + } + }) + .start(); + } - } + public void getPaletteAsync(OnPaletteLoadedListener onPaletteLoadedListener, Bitmap bitmap) { + this.drawable = new BitmapDrawable(context.getResources(), bitmap); + getPaletteAsync(onPaletteLoadedListener, this.drawable); + } - private int selectForegroundColor(int backgroundColor, Palette palette) { - if (isColorLight(backgroundColor)) { - return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(), - palette.getVibrantSwatch(), - palette.getDarkMutedSwatch(), - palette.getMutedSwatch(), - palette.getDominantSwatch(), - Color.BLACK); - } else { - return selectForegroundColorForSwatches(palette.getLightVibrantSwatch(), - palette.getVibrantSwatch(), - palette.getLightMutedSwatch(), - palette.getMutedSwatch(), - palette.getDominantSwatch(), - Color.WHITE); - } - } - - public boolean isLight() { - return isColorLight(backgroundColor); - } - - private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, - Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch, - Palette.Swatch dominantSwatch, int fallbackColor) { - Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant); - if (coloredCandidate == null) { - coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch); - } - if (coloredCandidate != null) { - if (dominantSwatch == coloredCandidate) { - return coloredCandidate.getRgb(); - } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation() - < POPULATION_FRACTION_FOR_DOMINANT - && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) { - return dominantSwatch.getRgb(); - } else { - return coloredCandidate.getRgb(); - } - } else if (hasEnoughPopulation(dominantSwatch)) { - return dominantSwatch.getRgb(); - } else { - return fallbackColor; - } - } - - private Palette.Swatch selectMutedCandidate(Palette.Swatch first, - Palette.Swatch second) { - boolean firstValid = hasEnoughPopulation(first); - boolean secondValid = hasEnoughPopulation(second); - if (firstValid && secondValid) { - float firstSaturation = first.getHsl()[1]; - float secondSaturation = second.getHsl()[1]; - float populationFraction = first.getPopulation() / (float) second.getPopulation(); - if (firstSaturation * populationFraction > secondSaturation) { - return first; - } else { - return second; - } - } else if (firstValid) { - return first; - } else if (secondValid) { - return second; - } - return null; - } - - private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { - boolean firstValid = hasEnoughPopulation(first); - boolean secondValid = hasEnoughPopulation(second); - if (firstValid && secondValid) { - int firstPopulation = first.getPopulation(); - int secondPopulation = second.getPopulation(); - if (firstPopulation / (float) secondPopulation - < POPULATION_FRACTION_FOR_MORE_VIBRANT) { - return second; - } else { - return first; - } - } else if (firstValid) { - return first; - } else if (secondValid) { - return second; - } - return null; - } - - private boolean hasEnoughPopulation(Palette.Swatch swatch) { - // We want a fraction that is at least 1% of the image - return swatch != null - && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); - } - - public int findBackgroundColorAndFilter(Drawable drawable) { - int width = drawable.getIntrinsicWidth(); - int height = drawable.getIntrinsicHeight(); - int area = width * height; + /** Processes a drawable and calculates the appropriate colors that should be used. */ + private void getMediaPalette() { + Bitmap bitmap; + if (drawable != null) { + // We're transforming the builder, let's make sure all baked in RemoteViews are + // rebuilt! + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + int area = width * height; + if (area > RESIZE_BITMAP_AREA) { double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); width = (int) (factor * width); height = (int) (factor * height); - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, width, height); drawable.draw(canvas); // for the background we only take the left side of the image to ensure // a smooth transition - Palette.Builder paletteBuilder = Palette.from(bitmap) + Palette.Builder paletteBuilder = + Palette.from(bitmap) .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) .clearFilters() // we want all colors, red / white / black ones too! .resizeBitmapArea(RESIZE_BITMAP_AREA); Palette palette = paletteBuilder.generate(); - // by default we use the dominant palette - Palette.Swatch dominantSwatch = palette.getDominantSwatch(); - if (dominantSwatch == null) { - // We're not filtering on white or black - mFilteredBackgroundHsl = null; - return Color.WHITE; + backgroundColor = findBackgroundColorAndFilter(drawable); + // we want most of the full region again, slightly shifted to the right + float textColorStartWidthFraction = 0.4f; + paletteBuilder.setRegion( + (int) (bitmap.getWidth() * textColorStartWidthFraction), + 0, + bitmap.getWidth(), + bitmap.getHeight()); + if (mFilteredBackgroundHsl != null) { + paletteBuilder.addFilter( + new Palette.Filter() { + @Override + public boolean isAllowed(int rgb, @NonNull float[] hsl) { + // at least 10 degrees hue difference + float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); + return diff > 10 && diff < 350; + } + }); } + paletteBuilder.addFilter(mBlackWhiteFilter); + palette = paletteBuilder.generate(); + int foregroundColor = selectForegroundColor(backgroundColor, palette); + ensureColors(backgroundColor, foregroundColor); + } + } + } - if (!isWhiteOrBlack(dominantSwatch.getHsl())) { - mFilteredBackgroundHsl = dominantSwatch.getHsl(); - return dominantSwatch.getRgb(); - } - // Oh well, we selected black or white. Lets look at the second color! - List swatches = palette.getSwatches(); - float highestNonWhitePopulation = -1; - Palette.Swatch second = null; - for (Palette.Swatch swatch : swatches) { - if (swatch != dominantSwatch - && swatch.getPopulation() > highestNonWhitePopulation - && !isWhiteOrBlack(swatch.getHsl())) { - second = swatch; - highestNonWhitePopulation = swatch.getPopulation(); - } - } - if (second == null) { - // We're not filtering on white or black - mFilteredBackgroundHsl = null; - return dominantSwatch.getRgb(); - } - if (dominantSwatch.getPopulation() / highestNonWhitePopulation - > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { - // The dominant swatch is very dominant, lets take it! - // We're not filtering on white or black - mFilteredBackgroundHsl = null; - return dominantSwatch.getRgb(); + private int selectForegroundColor(int backgroundColor, Palette palette) { + if (isColorLight(backgroundColor)) { + return selectForegroundColorForSwatches( + palette.getDarkVibrantSwatch(), + palette.getVibrantSwatch(), + palette.getDarkMutedSwatch(), + palette.getMutedSwatch(), + palette.getDominantSwatch(), + Color.BLACK); + } else { + return selectForegroundColorForSwatches( + palette.getLightVibrantSwatch(), + palette.getVibrantSwatch(), + palette.getLightMutedSwatch(), + palette.getMutedSwatch(), + palette.getDominantSwatch(), + Color.WHITE); + } + } + + public boolean isLight() { + return isColorLight(backgroundColor); + } + + private int selectForegroundColorForSwatches( + Palette.Swatch moreVibrant, + Palette.Swatch vibrant, + Palette.Swatch moreMutedSwatch, + Palette.Swatch mutedSwatch, + Palette.Swatch dominantSwatch, + int fallbackColor) { + Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant); + if (coloredCandidate == null) { + coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch); + } + if (coloredCandidate != null) { + if (dominantSwatch == coloredCandidate) { + return coloredCandidate.getRgb(); + } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation() + < POPULATION_FRACTION_FOR_DOMINANT + && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) { + return dominantSwatch.getRgb(); + } else { + return coloredCandidate.getRgb(); + } + } else if (hasEnoughPopulation(dominantSwatch)) { + return dominantSwatch.getRgb(); + } else { + return fallbackColor; + } + } + + private Palette.Swatch selectMutedCandidate(Palette.Swatch first, Palette.Swatch second) { + boolean firstValid = hasEnoughPopulation(first); + boolean secondValid = hasEnoughPopulation(second); + if (firstValid && secondValid) { + float firstSaturation = first.getHsl()[1]; + float secondSaturation = second.getHsl()[1]; + float populationFraction = first.getPopulation() / (float) second.getPopulation(); + if (firstSaturation * populationFraction > secondSaturation) { + return first; + } else { + return second; + } + } else if (firstValid) { + return first; + } else if (secondValid) { + return second; + } + return null; + } + + private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { + boolean firstValid = hasEnoughPopulation(first); + boolean secondValid = hasEnoughPopulation(second); + if (firstValid && secondValid) { + int firstPopulation = first.getPopulation(); + int secondPopulation = second.getPopulation(); + if (firstPopulation / (float) secondPopulation < POPULATION_FRACTION_FOR_MORE_VIBRANT) { + return second; + } else { + return first; + } + } else if (firstValid) { + return first; + } else if (secondValid) { + return second; + } + return null; + } + + private boolean hasEnoughPopulation(Palette.Swatch swatch) { + // We want a fraction that is at least 1% of the image + return swatch != null + && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); + } + + public int findBackgroundColorAndFilter(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + int area = width * height; + + double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); + width = (int) (factor * width); + height = (int) (factor * height); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, width, height); + drawable.draw(canvas); + + // for the background we only take the left side of the image to ensure + // a smooth transition + Palette.Builder paletteBuilder = + Palette.from(bitmap) + .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) + .clearFilters() // we want all colors, red / white / black ones too! + .resizeBitmapArea(RESIZE_BITMAP_AREA); + Palette palette = paletteBuilder.generate(); + // by default we use the dominant palette + Palette.Swatch dominantSwatch = palette.getDominantSwatch(); + if (dominantSwatch == null) { + // We're not filtering on white or black + mFilteredBackgroundHsl = null; + return Color.WHITE; + } + + if (!isWhiteOrBlack(dominantSwatch.getHsl())) { + mFilteredBackgroundHsl = dominantSwatch.getHsl(); + return dominantSwatch.getRgb(); + } + // Oh well, we selected black or white. Lets look at the second color! + List swatches = palette.getSwatches(); + float highestNonWhitePopulation = -1; + Palette.Swatch second = null; + for (Palette.Swatch swatch : swatches) { + if (swatch != dominantSwatch + && swatch.getPopulation() > highestNonWhitePopulation + && !isWhiteOrBlack(swatch.getHsl())) { + second = swatch; + highestNonWhitePopulation = swatch.getPopulation(); + } + } + if (second == null) { + // We're not filtering on white or black + mFilteredBackgroundHsl = null; + return dominantSwatch.getRgb(); + } + if (dominantSwatch.getPopulation() / highestNonWhitePopulation + > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { + // The dominant swatch is very dominant, lets take it! + // We're not filtering on white or black + mFilteredBackgroundHsl = null; + return dominantSwatch.getRgb(); + } else { + mFilteredBackgroundHsl = second.getHsl(); + return second.getRgb(); + } + } + + private boolean isWhiteOrBlack(float[] hsl) { + return isBlack(hsl) || isWhite(hsl); + } + + /** @return true if the color represents a color which is close to black. */ + private boolean isBlack(float[] hslColor) { + return hslColor[2] <= BLACK_MAX_LIGHTNESS; + } + + /** @return true if the color represents a color which is close to white. */ + private boolean isWhite(float[] hslColor) { + return hslColor[2] >= WHITE_MIN_LIGHTNESS; + } + + public void setIsLowPriority(boolean isLowPriority) { + mIsLowPriority = isLowPriority; + } + + private void ensureColors(int backgroundColor, int mForegroundColor) { + { + double backLum = NotificationColorUtil.calculateLuminance(backgroundColor); + double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor); + double contrast = NotificationColorUtil.calculateContrast(mForegroundColor, backgroundColor); + // We only respect the given colors if worst case Black or White still has + // contrast + boolean backgroundLight = + backLum > textLum + && NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.BLACK) + || backLum <= textLum + && !NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.WHITE); + if (contrast < 4.5f) { + if (backgroundLight) { + secondaryTextColor = + NotificationColorUtil.findContrastColor( + mForegroundColor, backgroundColor, true /* findFG */, 4.5f); + primaryTextColor = + NotificationColorUtil.changeColorLightness( + secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT); } else { - mFilteredBackgroundHsl = second.getHsl(); - return second.getRgb(); + secondaryTextColor = + NotificationColorUtil.findContrastColorAgainstDark( + mForegroundColor, backgroundColor, true /* findFG */, 4.5f); + primaryTextColor = + NotificationColorUtil.changeColorLightness( + secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK); } - } - - private boolean isWhiteOrBlack(float[] hsl) { - return isBlack(hsl) || isWhite(hsl); - } - - /** - * @return true if the color represents a color which is close to black. - */ - private boolean isBlack(float[] hslColor) { - return hslColor[2] <= BLACK_MAX_LIGHTNESS; - } - - /** - * @return true if the color represents a color which is close to white. - */ - private boolean isWhite(float[] hslColor) { - return hslColor[2] >= WHITE_MIN_LIGHTNESS; - } - - public void setIsLowPriority(boolean isLowPriority) { - mIsLowPriority = isLowPriority; - } - - private void ensureColors(int backgroundColor, int mForegroundColor) { - { - double backLum = NotificationColorUtil.calculateLuminance(backgroundColor); - double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor); - double contrast = NotificationColorUtil.calculateContrast(mForegroundColor, - backgroundColor); - // We only respect the given colors if worst case Black or White still has - // contrast - boolean backgroundLight = backLum > textLum - && NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.BLACK) - || backLum <= textLum - && !NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.WHITE); - if (contrast < 4.5f) { - if (backgroundLight) { - secondaryTextColor = NotificationColorUtil.findContrastColor( - mForegroundColor, - backgroundColor, - true /* findFG */, - 4.5f); - primaryTextColor = NotificationColorUtil.changeColorLightness( - secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT); - } else { - secondaryTextColor = - NotificationColorUtil.findContrastColorAgainstDark( - mForegroundColor, - backgroundColor, - true /* findFG */, - 4.5f); - primaryTextColor = NotificationColorUtil.changeColorLightness( - secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK); - } - } else { - primaryTextColor = mForegroundColor; - secondaryTextColor = NotificationColorUtil.changeColorLightness( - primaryTextColor, backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT - : LIGHTNESS_TEXT_DIFFERENCE_DARK); - if (NotificationColorUtil.calculateContrast(secondaryTextColor, - backgroundColor) < 4.5f) { - // oh well the secondary is not good enough - if (backgroundLight) { - secondaryTextColor = NotificationColorUtil.findContrastColor( - secondaryTextColor, - backgroundColor, - true /* findFG */, - 4.5f); - } else { - secondaryTextColor - = NotificationColorUtil.findContrastColorAgainstDark( - secondaryTextColor, - backgroundColor, - true /* findFG */, - 4.5f); - } - primaryTextColor = NotificationColorUtil.changeColorLightness( - secondaryTextColor, backgroundLight - ? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT - : -LIGHTNESS_TEXT_DIFFERENCE_DARK); - } - } + } else { + primaryTextColor = mForegroundColor; + secondaryTextColor = + NotificationColorUtil.changeColorLightness( + primaryTextColor, + backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT : LIGHTNESS_TEXT_DIFFERENCE_DARK); + if (NotificationColorUtil.calculateContrast(secondaryTextColor, backgroundColor) < 4.5f) { + // oh well the secondary is not good enough + if (backgroundLight) { + secondaryTextColor = + NotificationColorUtil.findContrastColor( + secondaryTextColor, backgroundColor, true /* findFG */, 4.5f); + } else { + secondaryTextColor = + NotificationColorUtil.findContrastColorAgainstDark( + secondaryTextColor, backgroundColor, true /* findFG */, 4.5f); + } + primaryTextColor = + NotificationColorUtil.changeColorLightness( + secondaryTextColor, + backgroundLight + ? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT + : -LIGHTNESS_TEXT_DIFFERENCE_DARK); } - actionBarColor = NotificationColorUtil.resolveActionBarColor(context, - backgroundColor); + } } + actionBarColor = NotificationColorUtil.resolveActionBarColor(context, backgroundColor); + } - public int getPrimaryTextColor() { + public int getPrimaryTextColor() { + return primaryTextColor; + } + + public int getSecondaryTextColor() { + return secondaryTextColor; + } + + public int getActionBarColor() { + return actionBarColor; + } + + public int getBackgroundColor() { + return backgroundColor; + } + + boolean isWhiteColor(int color) { + return calculateLuminance(color) > 0.6f; + } + + public int getMightyColor() { + boolean isDarkBg = + ColorUtil.INSTANCE.isColorLight( + ATHUtil.INSTANCE.resolveColor(context, R.attr.colorSurface)); + if (isDarkBg) { + if (isColorLight(backgroundColor)) { return primaryTextColor; - } - - public int getSecondaryTextColor() { - return secondaryTextColor; - } - - public int getActionBarColor() { - return actionBarColor; - } - - public int getBackgroundColor() { + } else { return backgroundColor; + } + } else { + if (isColorLight(backgroundColor)) { + return backgroundColor; + } else { + return primaryTextColor; + } } + } - boolean isWhiteColor(int color) { - return calculateLuminance(color) > 0.6f; - } - - public int getMightyColor() { - boolean isDarkBg = ColorUtil.INSTANCE.isColorLight(ATHUtil.INSTANCE.resolveColor(context, R.attr.colorSurface)); - if (isDarkBg) { - if (isColorLight(backgroundColor)) { - return primaryTextColor; - } else { - return backgroundColor; - } - } else { - if (isColorLight(backgroundColor)) { - return backgroundColor; - } else { - return primaryTextColor; - } - } - } - - public interface OnPaletteLoadedListener { - void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor); - } -} \ No newline at end of file + public interface OnPaletteLoadedListener { + void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java index 4d607f82b..fe8e4346d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/color/NotificationColorUtil.java @@ -28,962 +28,966 @@ import android.text.style.ForegroundColorSpan; import android.text.style.TextAppearanceSpan; import android.util.Log; import android.util.Pair; - import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; - +import code.name.monkey.retromusic.R; import java.util.WeakHashMap; -import code.name.monkey.retromusic.R; - /** - * Helper class to process legacy (Holo) notifications to make them look like material notifications. + * Helper class to process legacy (Holo) notifications to make them look like material + * notifications. * * @hide */ public class NotificationColorUtil { - private static final String TAG = "NotificationColorUtil"; - private static final boolean DEBUG = false; + private static final String TAG = "NotificationColorUtil"; + private static final boolean DEBUG = false; - private static final Object sLock = new Object(); - private static NotificationColorUtil sInstance; + private static final Object sLock = new Object(); + private static NotificationColorUtil sInstance; - private final ImageUtils mImageUtils = new ImageUtils(); - private final WeakHashMap> mGrayscaleBitmapCache = - new WeakHashMap>(); + private final ImageUtils mImageUtils = new ImageUtils(); + private final WeakHashMap> mGrayscaleBitmapCache = + new WeakHashMap>(); - private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp) + private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp) - private NotificationColorUtil(Context context) { - mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize( - R.dimen.notification_large_icon_width); + private NotificationColorUtil(Context context) { + mGrayscaleIconMaxSize = + context.getResources().getDimensionPixelSize(R.dimen.notification_large_icon_width); + } + + public static NotificationColorUtil getInstance(Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new NotificationColorUtil(context); + } + return sInstance; } + } - public static NotificationColorUtil getInstance(Context context) { - synchronized (sLock) { - if (sInstance == null) { - sInstance = new NotificationColorUtil(context); - } - return sInstance; + /** + * Clears all color spans of a text + * + * @param charSequence the input text + * @return the same text but without color spans + */ + public static CharSequence clearColorSpans(CharSequence charSequence) { + if (charSequence instanceof Spanned) { + Spanned ss = (Spanned) charSequence; + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + Object resultSpan = span; + if (resultSpan instanceof CharacterStyle) { + resultSpan = ((CharacterStyle) span).getUnderlying(); } - } - - /** - * Clears all color spans of a text - * - * @param charSequence the input text - * @return the same text but without color spans - */ - public static CharSequence clearColorSpans(CharSequence charSequence) { - if (charSequence instanceof Spanned) { - Spanned ss = (Spanned) charSequence; - Object[] spans = ss.getSpans(0, ss.length(), Object.class); - SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); - for (Object span : spans) { - Object resultSpan = span; - if (resultSpan instanceof CharacterStyle) { - resultSpan = ((CharacterStyle) span).getUnderlying(); - } - if (resultSpan instanceof TextAppearanceSpan) { - TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; - if (originalSpan.getTextColor() != null) { - resultSpan = new TextAppearanceSpan( - originalSpan.getFamily(), - originalSpan.getTextStyle(), - originalSpan.getTextSize(), - null, - originalSpan.getLinkTextColor()); - } - } else if (resultSpan instanceof ForegroundColorSpan - || (resultSpan instanceof BackgroundColorSpan)) { - continue; - } else { - resultSpan = span; - } - builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), - ss.getSpanFlags(span)); - } - return builder; - } - return charSequence; - } - - -// /** -// * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on -// * the text. -// * -// * @param charSequence The text to process. -// * @return The color inverted text. -// */ -// public CharSequence invertCharSequenceColors(CharSequence charSequence) { -// if (charSequence instanceof Spanned) { -// Spanned ss = (Spanned) charSequence; -// Object[] spans = ss.getSpans(0, ss.length(), Object.class); -// SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); -// for (Object span : spans) { -// Object resultSpan = span; -// if (resultSpan instanceof CharacterStyle) { -// resultSpan = ((CharacterStyle) span).getUnderlying(); -// } -// if (resultSpan instanceof TextAppearanceSpan) { -// TextAppearanceSpan processedSpan = processTextAppearanceSpan( -// (TextAppearanceSpan) span); -// if (processedSpan != resultSpan) { -// resultSpan = processedSpan; -// } else { -// // we need to still take the orgininal for wrapped spans -// resultSpan = span; -// } -// } else if (resultSpan instanceof ForegroundColorSpan) { -// ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan; -// int foregroundColor = originalSpan.getForegroundColor(); -// resultSpan = new ForegroundColorSpan(processColor(foregroundColor)); -// } else { -// resultSpan = span; -// } -// builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), -// ss.getSpanFlags(span)); -// } -// return builder; -// } -// return charSequence; -// } - -// private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { -// ColorStateList colorStateList = span.getTextColor(); -// if (colorStateList != null) { -// int[] colors = colorStateList.getColors(); -// boolean changed = false; -// for (int i = 0; i < colors.length; i++) { -// if (ImageUtils.isGrayscale(colors[i])) { -// -// // Allocate a new array so we don't change the colors in the old color state -// // list. -// if (!changed) { -// colors = Arrays.copyOf(colors, colors.length); -// } -// colors[i] = processColor(colors[i]); -// changed = true; -// } -// } -// if (changed) { -// return new TextAppearanceSpan( -// span.getFamily(), span.getTextStyle(), span.getTextSize(), -// new ColorStateList(colorStateList.getStates(), colors), -// span.getLinkTextColor()); -// } -// } -// return span; -// } - - /** - * Finds a suitable color such that there's enough contrast. - * - * @param color the color to start searching from. - * @param other the color to ensure contrast against. Assumed to be lighter than {@param color} - * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. - * @param minRatio the minimum contrast ratio required. - * @return a color with the same hue as {@param color}, potentially darkened to meet the - * contrast ratio. - */ - public static int findContrastColor(int color, int other, boolean findFg, double minRatio) { - int fg = findFg ? color : other; - int bg = findFg ? other : color; - if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { - return color; - } - - double[] lab = new double[3]; - ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab); - - double low = 0, high = lab[0]; - final double a = lab[1], b = lab[2]; - for (int i = 0; i < 15 && high - low > 0.00001; i++) { - final double l = (low + high) / 2; - if (findFg) { - fg = ColorUtilsFromCompat.LABToColor(l, a, b); - } else { - bg = ColorUtilsFromCompat.LABToColor(l, a, b); - } - if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { - low = l; - } else { - high = l; - } - } - return ColorUtilsFromCompat.LABToColor(low, a, b); - } - - /** - * Finds a suitable alpha such that there's enough contrast. - * - * @param color the color to start searching from. - * @param backgroundColor the color to ensure contrast against. - * @param minRatio the minimum contrast ratio required. - * @return the same color as {@param color} with potentially modified alpha to meet contrast - */ - public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) { - int fg = color; - int bg = backgroundColor; - if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { - return color; - } - int startAlpha = Color.alpha(color); - int r = Color.red(color); - int g = Color.green(color); - int b = Color.blue(color); - - int low = startAlpha, high = 255; - for (int i = 0; i < 15 && high - low > 0; i++) { - final int alpha = (low + high) / 2; - fg = Color.argb(alpha, r, g, b); - if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { - high = alpha; - } else { - low = alpha; - } - } - return Color.argb(high, r, g, b); - } - - /** - * Finds a suitable color such that there's enough contrast. - * - * @param color the color to start searching from. - * @param other the color to ensure contrast against. Assumed to be darker than {@param color} - * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. - * @param minRatio the minimum contrast ratio required. - * @return a color with the same hue as {@param color}, potentially darkened to meet the - * contrast ratio. - */ - public static int findContrastColorAgainstDark(int color, int other, boolean findFg, - double minRatio) { - int fg = findFg ? color : other; - int bg = findFg ? other : color; - if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { - return color; - } - - float[] hsl = new float[3]; - ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl); - - float low = hsl[2], high = 1; - for (int i = 0; i < 15 && high - low > 0.00001; i++) { - final float l = (low + high) / 2; - hsl[2] = l; - if (findFg) { - fg = ColorUtilsFromCompat.HSLToColor(hsl); - } else { - bg = ColorUtilsFromCompat.HSLToColor(hsl); - } - if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { - high = l; - } else { - low = l; - } - } - return findFg ? fg : bg; - } - - public static int ensureTextContrastOnBlack(int color) { - return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12); - } - - /** - * Finds a large text color with sufficient contrast over bg that has the same or darker hue as - * the original color, depending on the value of {@code isBgDarker}. - * - * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. - */ - public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) { - return isBgDarker - ? findContrastColorAgainstDark(color, bg, true, 3) - : findContrastColor(color, bg, true, 3); - } - - /** - * Finds a text color with sufficient contrast over bg that has the same or darker hue as the - * original color, depending on the value of {@code isBgDarker}. - * - * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. - */ - private static int ensureTextContrast(int color, int bg, boolean isBgDarker) { - return isBgDarker - ? findContrastColorAgainstDark(color, bg, true, 4.5) - : findContrastColor(color, bg, true, 4.5); - } - - /** - * Finds a background color for a text view with given text color and hint text color, that - * has the same hue as the original color. - */ - public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) { - color = findContrastColor(color, hintColor, false, 3.0); - return findContrastColor(color, textColor, false, 4.5); - } - - private static String contrastChange(int colorOld, int colorNew, int bg) { - return String.format("from %.2f:1 to %.2f:1", - ColorUtilsFromCompat.calculateContrast(colorOld, bg), - ColorUtilsFromCompat.calculateContrast(colorNew, bg)); - } - - /** - * Change a color by a specified value - * - * @param baseColor the base color to lighten - * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L - * increase in the LAB color space. A negative value will darken the color and - * a positive will lighten it. - * @return the changed color - */ - public static int changeColorLightness(int baseColor, int amount) { - final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); - ColorUtilsFromCompat.colorToLAB(baseColor, result); - result[0] = Math.max(Math.min(100, result[0] + amount), 0); - return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); - } - - public static int resolvePrimaryColor(Context context, int backgroundColor) { - boolean useDark = shouldUseDark(backgroundColor); - if (useDark) { - return ContextCompat.getColor(context, android.R.color.primary_text_light); + if (resultSpan instanceof TextAppearanceSpan) { + TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; + if (originalSpan.getTextColor() != null) { + resultSpan = + new TextAppearanceSpan( + originalSpan.getFamily(), + originalSpan.getTextStyle(), + originalSpan.getTextSize(), + null, + originalSpan.getLinkTextColor()); + } + } else if (resultSpan instanceof ForegroundColorSpan + || (resultSpan instanceof BackgroundColorSpan)) { + continue; } else { - return ContextCompat.getColor(context, android.R.color.primary_text_light); + resultSpan = span; } + builder.setSpan( + resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), ss.getSpanFlags(span)); + } + return builder; + } + return charSequence; + } + + // /** + // * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on + // * the text. + // * + // * @param charSequence The text to process. + // * @return The color inverted text. + // */ + // public CharSequence invertCharSequenceColors(CharSequence charSequence) { + // if (charSequence instanceof Spanned) { + // Spanned ss = (Spanned) charSequence; + // Object[] spans = ss.getSpans(0, ss.length(), Object.class); + // SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + // for (Object span : spans) { + // Object resultSpan = span; + // if (resultSpan instanceof CharacterStyle) { + // resultSpan = ((CharacterStyle) span).getUnderlying(); + // } + // if (resultSpan instanceof TextAppearanceSpan) { + // TextAppearanceSpan processedSpan = processTextAppearanceSpan( + // (TextAppearanceSpan) span); + // if (processedSpan != resultSpan) { + // resultSpan = processedSpan; + // } else { + // // we need to still take the orgininal for wrapped spans + // resultSpan = span; + // } + // } else if (resultSpan instanceof ForegroundColorSpan) { + // ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan; + // int foregroundColor = originalSpan.getForegroundColor(); + // resultSpan = new ForegroundColorSpan(processColor(foregroundColor)); + // } else { + // resultSpan = span; + // } + // builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + // ss.getSpanFlags(span)); + // } + // return builder; + // } + // return charSequence; + // } + + // private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { + // ColorStateList colorStateList = span.getTextColor(); + // if (colorStateList != null) { + // int[] colors = colorStateList.getColors(); + // boolean changed = false; + // for (int i = 0; i < colors.length; i++) { + // if (ImageUtils.isGrayscale(colors[i])) { + // + // // Allocate a new array so we don't change the colors in the old color state + // // list. + // if (!changed) { + // colors = Arrays.copyOf(colors, colors.length); + // } + // colors[i] = processColor(colors[i]); + // changed = true; + // } + // } + // if (changed) { + // return new TextAppearanceSpan( + // span.getFamily(), span.getTextStyle(), span.getTextSize(), + // new ColorStateList(colorStateList.getStates(), colors), + // span.getLinkTextColor()); + // } + // } + // return span; + // } + + /** + * Finds a suitable color such that there's enough contrast. + * + * @param color the color to start searching from. + * @param other the color to ensure contrast against. Assumed to be lighter than {@param color} + * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. + * @param minRatio the minimum contrast ratio required. + * @return a color with the same hue as {@param color}, potentially darkened to meet the contrast + * ratio. + */ + public static int findContrastColor(int color, int other, boolean findFg, double minRatio) { + int fg = findFg ? color : other; + int bg = findFg ? other : color; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; } + double[] lab = new double[3]; + ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab); - public static int resolveSecondaryColor(Context context, int backgroundColor) { - boolean useDark = shouldUseDark(backgroundColor); - if (useDark) { - return ContextCompat.getColor(context, - android.R.color.secondary_text_light); - } else { - return ContextCompat.getColor(context, android.R.color.secondary_text_dark); - } + double low = 0, high = lab[0]; + final double a = lab[1], b = lab[2]; + for (int i = 0; i < 15 && high - low > 0.00001; i++) { + final double l = (low + high) / 2; + if (findFg) { + fg = ColorUtilsFromCompat.LABToColor(l, a, b); + } else { + bg = ColorUtilsFromCompat.LABToColor(l, a, b); + } + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + low = l; + } else { + high = l; + } + } + return ColorUtilsFromCompat.LABToColor(low, a, b); + } + + /** + * Finds a suitable alpha such that there's enough contrast. + * + * @param color the color to start searching from. + * @param backgroundColor the color to ensure contrast against. + * @param minRatio the minimum contrast ratio required. + * @return the same color as {@param color} with potentially modified alpha to meet contrast + */ + public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) { + int fg = color; + int bg = backgroundColor; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; + } + int startAlpha = Color.alpha(color); + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + int low = startAlpha, high = 255; + for (int i = 0; i < 15 && high - low > 0; i++) { + final int alpha = (low + high) / 2; + fg = Color.argb(alpha, r, g, b); + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + high = alpha; + } else { + low = alpha; + } + } + return Color.argb(high, r, g, b); + } + + /** + * Finds a suitable color such that there's enough contrast. + * + * @param color the color to start searching from. + * @param other the color to ensure contrast against. Assumed to be darker than {@param color} + * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. + * @param minRatio the minimum contrast ratio required. + * @return a color with the same hue as {@param color}, potentially darkened to meet the contrast + * ratio. + */ + public static int findContrastColorAgainstDark( + int color, int other, boolean findFg, double minRatio) { + int fg = findFg ? color : other; + int bg = findFg ? other : color; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; } - public static int resolveActionBarColor(Context context, int backgroundColor) { - if (backgroundColor == Notification.COLOR_DEFAULT) { - return Color.BLACK; + float[] hsl = new float[3]; + ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl); + + float low = hsl[2], high = 1; + for (int i = 0; i < 15 && high - low > 0.00001; i++) { + final float l = (low + high) / 2; + hsl[2] = l; + if (findFg) { + fg = ColorUtilsFromCompat.HSLToColor(hsl); + } else { + bg = ColorUtilsFromCompat.HSLToColor(hsl); + } + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + high = l; + } else { + low = l; + } + } + return findFg ? fg : bg; + } + + public static int ensureTextContrastOnBlack(int color) { + return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12); + } + + /** + * Finds a large text color with sufficient contrast over bg that has the same or darker hue as + * the original color, depending on the value of {@code isBgDarker}. + * + * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. + */ + public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) { + return isBgDarker + ? findContrastColorAgainstDark(color, bg, true, 3) + : findContrastColor(color, bg, true, 3); + } + + /** + * Finds a text color with sufficient contrast over bg that has the same or darker hue as the + * original color, depending on the value of {@code isBgDarker}. + * + * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. + */ + private static int ensureTextContrast(int color, int bg, boolean isBgDarker) { + return isBgDarker + ? findContrastColorAgainstDark(color, bg, true, 4.5) + : findContrastColor(color, bg, true, 4.5); + } + + /** + * Finds a background color for a text view with given text color and hint text color, that has + * the same hue as the original color. + */ + public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) { + color = findContrastColor(color, hintColor, false, 3.0); + return findContrastColor(color, textColor, false, 4.5); + } + + private static String contrastChange(int colorOld, int colorNew, int bg) { + return String.format( + "from %.2f:1 to %.2f:1", + ColorUtilsFromCompat.calculateContrast(colorOld, bg), + ColorUtilsFromCompat.calculateContrast(colorNew, bg)); + } + + /** + * Change a color by a specified value + * + * @param baseColor the base color to lighten + * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L increase + * in the LAB color space. A negative value will darken the color and a positive will lighten + * it. + * @return the changed color + */ + public static int changeColorLightness(int baseColor, int amount) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(baseColor, result); + result[0] = Math.max(Math.min(100, result[0] + amount), 0); + return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); + } + + public static int resolvePrimaryColor(Context context, int backgroundColor) { + boolean useDark = shouldUseDark(backgroundColor); + if (useDark) { + return ContextCompat.getColor(context, android.R.color.primary_text_light); + } else { + return ContextCompat.getColor(context, android.R.color.primary_text_light); + } + } + + public static int resolveSecondaryColor(Context context, int backgroundColor) { + boolean useDark = shouldUseDark(backgroundColor); + if (useDark) { + return ContextCompat.getColor(context, android.R.color.secondary_text_light); + } else { + return ContextCompat.getColor(context, android.R.color.secondary_text_dark); + } + } + + public static int resolveActionBarColor(Context context, int backgroundColor) { + if (backgroundColor == Notification.COLOR_DEFAULT) { + return Color.BLACK; + } + return getShiftedColor(backgroundColor, 7); + } + + /** Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} */ + public static int resolveColor(Context context, int color) { + if (color == Notification.COLOR_DEFAULT) { + return ContextCompat.getColor(context, android.R.color.background_dark); + } + return color; + } + + // + // public static int resolveContrastColor(Context context, int notificationColor, + // int backgroundColor) { + // return NotificationColorUtil.resolveContrastColor(context, notificationColor, + // backgroundColor, false /* isDark */); + // } + + // /** + // * Resolves a Notification's color such that it has enough contrast to be used as the + // * color for the Notification's action and header text. + // * + // * @param notificationColor the color of the notification or {@link + // Notification#COLOR_DEFAULT} + // * @param backgroundColor the background color to ensure the contrast against. + // * @param isDark whether or not the {@code notificationColor} will be placed on a background + // * that is darker than the color itself + // * @return a color of the same hue with enough contrast against the backgrounds. + // */ + // public static int resolveContrastColor(Context context, int notificationColor, + // int backgroundColor, boolean isDark) { + // final int resolvedColor = resolveColor(context, notificationColor); + // + // final int actionBg = context.getColor( + // com.android.internal.R.color.notification_action_list); + // + // int color = resolvedColor; + // color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark); + // color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark); + // + // if (color != resolvedColor) { + // if (DEBUG){ + // Log.w(TAG, String.format( + // "Enhanced contrast of notification for %s %s (over action)" + // + " and %s (over background) by changing #%s to %s", + // context.getPackageName(), + // NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), + // NotificationColorUtil.contrastChange(resolvedColor, color, + // backgroundColor), + // Integer.toHexString(resolvedColor), Integer.toHexString(color))); + // } + // } + // return color; + // } + + /** + * Get a color that stays in the same tint, but darkens or lightens it by a certain amount. This + * also looks at the lightness of the provided color and shifts it appropriately. + * + * @param color the base color to use + * @param amount the amount from 1 to 100 how much to modify the color + * @return the now color that was modified + */ + public static int getShiftedColor(int color, int amount) { + final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); + ColorUtilsFromCompat.colorToLAB(color, result); + if (result[0] >= 4) { + result[0] = Math.max(0, result[0] - amount); + } else { + result[0] = Math.min(100, result[0] + amount); + } + return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); + } + + public static int resolveAmbientColor(Context context, int notificationColor) { + final int resolvedColor = resolveColor(context, notificationColor); + + int color = resolvedColor; + color = NotificationColorUtil.ensureTextContrastOnBlack(color); + + if (color != resolvedColor) { + if (DEBUG) { + Log.w( + TAG, + String.format( + "Ambient contrast of notification for %s is %s (over black)" + + " by changing #%s to #%s", + context.getPackageName(), + NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK), + Integer.toHexString(resolvedColor), + Integer.toHexString(color))); + } + } + return color; + } + + private static boolean shouldUseDark(int backgroundColor) { + boolean useDark = backgroundColor == Notification.COLOR_DEFAULT; + if (!useDark) { + useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; + } + return useDark; + } + + public static double calculateLuminance(int backgroundColor) { + return ColorUtilsFromCompat.calculateLuminance(backgroundColor); + } + + public static double calculateContrast(int foregroundColor, int backgroundColor) { + return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor); + } + + public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) { + return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5; + } + + /** Composite two potentially translucent colors over each other and returns the result. */ + public static int compositeColors(int foreground, int background) { + return ColorUtilsFromCompat.compositeColors(foreground, background); + } + + public static boolean isColorLight(int backgroundColor) { + return calculateLuminance(backgroundColor) > 0.5f; + } + + /** + * Checks whether a Bitmap is a small grayscale icon. Grayscale here means "very close to a + * perfect gray"; icon means "no larger than 64dp". + * + * @param bitmap The bitmap to test. + * @return True if the bitmap is grayscale; false if it is color or too large to examine. + */ + public boolean isGrayscaleIcon(Bitmap bitmap) { + // quick test: reject large bitmaps + if (bitmap.getWidth() > mGrayscaleIconMaxSize || bitmap.getHeight() > mGrayscaleIconMaxSize) { + return false; + } + + synchronized (sLock) { + Pair cached = mGrayscaleBitmapCache.get(bitmap); + if (cached != null) { + if (cached.second == bitmap.getGenerationId()) { + return cached.first; } - return getShiftedColor(backgroundColor, 7); + } + } + boolean result; + int generationId; + synchronized (mImageUtils) { + result = mImageUtils.isGrayscale(bitmap); + + // generationId and the check whether the Bitmap is grayscale can't be read atomically + // here. However, since the thread is in the process of posting the notification, we can + // assume that it doesn't modify the bitmap while we are checking the pixels. + generationId = bitmap.getGenerationId(); + } + synchronized (sLock) { + mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); + } + return result; + } + + private int processColor(int color) { + return Color.argb( + Color.alpha(color), + 255 - Color.red(color), + 255 - Color.green(color), + 255 - Color.blue(color)); + } + + /** Framework copy of functions needed from android.support.v4.graphics.ColorUtils. */ + private static class ColorUtilsFromCompat { + private static final double XYZ_WHITE_REFERENCE_X = 95.047; + private static final double XYZ_WHITE_REFERENCE_Y = 100; + private static final double XYZ_WHITE_REFERENCE_Z = 108.883; + private static final double XYZ_EPSILON = 0.008856; + private static final double XYZ_KAPPA = 903.3; + + private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; + private static final int MIN_ALPHA_SEARCH_PRECISION = 1; + + private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); + + private ColorUtilsFromCompat() {} + + /** Composite two potentially translucent colors over each other and returns the result. */ + public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { + int bgAlpha = Color.alpha(background); + int fgAlpha = Color.alpha(foreground); + int a = compositeAlpha(fgAlpha, bgAlpha); + + int r = compositeComponent(Color.red(foreground), fgAlpha, Color.red(background), bgAlpha, a); + int g = + compositeComponent(Color.green(foreground), fgAlpha, Color.green(background), bgAlpha, a); + int b = + compositeComponent(Color.blue(foreground), fgAlpha, Color.blue(background), bgAlpha, a); + + return Color.argb(a, r, g, b); + } + + private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { + return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); + } + + private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { + if (a == 0) return 0; + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); } /** - * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} - */ - public static int resolveColor(Context context, int color) { - if (color == Notification.COLOR_DEFAULT) { - return ContextCompat.getColor(context, android.R.color.background_dark); - } - return color; - } - -// -// public static int resolveContrastColor(Context context, int notificationColor, -// int backgroundColor) { -// return NotificationColorUtil.resolveContrastColor(context, notificationColor, -// backgroundColor, false /* isDark */); -// } - -// /** -// * Resolves a Notification's color such that it has enough contrast to be used as the -// * color for the Notification's action and header text. -// * -// * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT} -// * @param backgroundColor the background color to ensure the contrast against. -// * @param isDark whether or not the {@code notificationColor} will be placed on a background -// * that is darker than the color itself -// * @return a color of the same hue with enough contrast against the backgrounds. -// */ -// public static int resolveContrastColor(Context context, int notificationColor, -// int backgroundColor, boolean isDark) { -// final int resolvedColor = resolveColor(context, notificationColor); -// -// final int actionBg = context.getColor( -// com.android.internal.R.color.notification_action_list); -// -// int color = resolvedColor; -// color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark); -// color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark); -// -// if (color != resolvedColor) { -// if (DEBUG){ -// Log.w(TAG, String.format( -// "Enhanced contrast of notification for %s %s (over action)" -// + " and %s (over background) by changing #%s to %s", -// context.getPackageName(), -// NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), -// NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor), -// Integer.toHexString(resolvedColor), Integer.toHexString(color))); -// } -// } -// return color; -// } - - /** - * Get a color that stays in the same tint, but darkens or lightens it by a certain - * amount. - * This also looks at the lightness of the provided color and shifts it appropriately. + * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. * - * @param color the base color to use - * @param amount the amount from 1 to 100 how much to modify the color - * @return the now color that was modified + *

Defined as the Y component in the XYZ representation of {@code color}. */ - public static int getShiftedColor(int color, int amount) { - final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); - ColorUtilsFromCompat.colorToLAB(color, result); - if (result[0] >= 4) { - result[0] = Math.max(0, result[0] - amount); - } else { - result[0] = Math.min(100, result[0] + amount); - } - return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); - } - - public static int resolveAmbientColor(Context context, int notificationColor) { - final int resolvedColor = resolveColor(context, notificationColor); - - int color = resolvedColor; - color = NotificationColorUtil.ensureTextContrastOnBlack(color); - - if (color != resolvedColor) { - if (DEBUG) { - Log.w(TAG, String.format( - "Ambient contrast of notification for %s is %s (over black)" - + " by changing #%s to #%s", - context.getPackageName(), - NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK), - Integer.toHexString(resolvedColor), Integer.toHexString(color))); - } - } - return color; - } - - private static boolean shouldUseDark(int backgroundColor) { - boolean useDark = backgroundColor == Notification.COLOR_DEFAULT; - if (!useDark) { - useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; - } - return useDark; - } - - public static double calculateLuminance(int backgroundColor) { - return ColorUtilsFromCompat.calculateLuminance(backgroundColor); - } - - public static double calculateContrast(int foregroundColor, int backgroundColor) { - return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor); - } - - public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) { - return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5; + @FloatRange(from = 0.0, to = 1.0) + public static double calculateLuminance(@ColorInt int color) { + final double[] result = getTempDouble3Array(); + colorToXYZ(color, result); + // Luminance is the Y component + return result[1] / 100; } /** - * Composite two potentially translucent colors over each other and returns the result. - */ - public static int compositeColors(int foreground, int background) { - return ColorUtilsFromCompat.compositeColors(foreground, background); - } - - public static boolean isColorLight(int backgroundColor) { - return calculateLuminance(backgroundColor) > 0.5f; - } - - /** - * Checks whether a Bitmap is a small grayscale icon. - * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp". + * Returns the contrast ratio between {@code foreground} and {@code background}. {@code + * background} must be opaque. * - * @param bitmap The bitmap to test. - * @return True if the bitmap is grayscale; false if it is color or too large to examine. + *

Formula defined here. */ - public boolean isGrayscaleIcon(Bitmap bitmap) { - // quick test: reject large bitmaps - if (bitmap.getWidth() > mGrayscaleIconMaxSize - || bitmap.getHeight() > mGrayscaleIconMaxSize) { - return false; - } + public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { + if (Color.alpha(background) != 255) { + Log.wtf(TAG, "background can not be translucent: #" + Integer.toHexString(background)); + } + if (Color.alpha(foreground) < 255) { + // If the foreground is translucent, composite the foreground over the background + foreground = compositeColors(foreground, background); + } - synchronized (sLock) { - Pair cached = mGrayscaleBitmapCache.get(bitmap); - if (cached != null) { - if (cached.second == bitmap.getGenerationId()) { - return cached.first; - } - } - } - boolean result; - int generationId; - synchronized (mImageUtils) { - result = mImageUtils.isGrayscale(bitmap); + final double luminance1 = calculateLuminance(foreground) + 0.05; + final double luminance2 = calculateLuminance(background) + 0.05; - // generationId and the check whether the Bitmap is grayscale can't be read atomically - // here. However, since the thread is in the process of posting the notification, we can - // assume that it doesn't modify the bitmap while we are checking the pixels. - generationId = bitmap.getGenerationId(); - } - synchronized (sLock) { - mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); - } - return result; - } - - private int processColor(int color) { - return Color.argb(Color.alpha(color), - 255 - Color.red(color), - 255 - Color.green(color), - 255 - Color.blue(color)); + // Now return the lighter luminance divided by the darker luminance + return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); } /** - * Framework copy of functions needed from android.support.v4.graphics.ColorUtils. + * Convert the ARGB color to its CIE Lab representative components. + * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outLab 3-element array which holds the resulting LAB components */ - private static class ColorUtilsFromCompat { - private static final double XYZ_WHITE_REFERENCE_X = 95.047; - private static final double XYZ_WHITE_REFERENCE_Y = 100; - private static final double XYZ_WHITE_REFERENCE_Z = 108.883; - private static final double XYZ_EPSILON = 0.008856; - private static final double XYZ_KAPPA = 903.3; - - private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; - private static final int MIN_ALPHA_SEARCH_PRECISION = 1; - - private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); - - private ColorUtilsFromCompat() { - } - - /** - * Composite two potentially translucent colors over each other and returns the result. - */ - public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { - int bgAlpha = Color.alpha(background); - int fgAlpha = Color.alpha(foreground); - int a = compositeAlpha(fgAlpha, bgAlpha); - - int r = compositeComponent(Color.red(foreground), fgAlpha, - Color.red(background), bgAlpha, a); - int g = compositeComponent(Color.green(foreground), fgAlpha, - Color.green(background), bgAlpha, a); - int b = compositeComponent(Color.blue(foreground), fgAlpha, - Color.blue(background), bgAlpha, a); - - return Color.argb(a, r, g, b); - } - - private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { - return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); - } - - private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { - if (a == 0) return 0; - return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); - } - - /** - * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. - *

Defined as the Y component in the XYZ representation of {@code color}.

- */ - @FloatRange(from = 0.0, to = 1.0) - public static double calculateLuminance(@ColorInt int color) { - final double[] result = getTempDouble3Array(); - colorToXYZ(color, result); - // Luminance is the Y component - return result[1] / 100; - } - - /** - * Returns the contrast ratio between {@code foreground} and {@code background}. - * {@code background} must be opaque. - *

- * Formula defined - * here. - */ - public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { - if (Color.alpha(background) != 255) { - Log.wtf(TAG, "background can not be translucent: #" - + Integer.toHexString(background)); - } - if (Color.alpha(foreground) < 255) { - // If the foreground is translucent, composite the foreground over the background - foreground = compositeColors(foreground, background); - } - - final double luminance1 = calculateLuminance(foreground) + 0.05; - final double luminance2 = calculateLuminance(background) + 0.05; - - // Now return the lighter luminance divided by the darker luminance - return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); - } - - /** - * Convert the ARGB color to its CIE Lab representative components. - * - * @param color the ARGB color to convert. The alpha component is ignored - * @param outLab 3-element array which holds the resulting LAB components - */ - public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { - RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); - } - - /** - * Convert RGB components to its CIE Lab representative components. - * - *

    - *
  • outLab[0] is L [0 ...100)
  • - *
  • outLab[1] is a [-128...127)
  • - *
  • outLab[2] is b [-128...127)
  • - *
- * - * @param r red component value [0..255] - * @param g green component value [0..255] - * @param b blue component value [0..255] - * @param outLab 3-element array which holds the resulting LAB components - */ - public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, - @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, - @NonNull double[] outLab) { - // First we convert RGB to XYZ - RGBToXYZ(r, g, b, outLab); - // outLab now contains XYZ - XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); - // outLab now contains LAB representation - } - - /** - * Convert the ARGB color to it's CIE XYZ representative components. - * - *

The resulting XYZ representation will use the D65 illuminant and the CIE - * 2° Standard Observer (1931).

- * - *
    - *
  • outXyz[0] is X [0 ...95.047)
  • - *
  • outXyz[1] is Y [0...100)
  • - *
  • outXyz[2] is Z [0...108.883)
  • - *
- * - * @param color the ARGB color to convert. The alpha component is ignored - * @param outXyz 3-element array which holds the resulting LAB components - */ - public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { - RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); - } - - /** - * Convert RGB components to it's CIE XYZ representative components. - * - *

The resulting XYZ representation will use the D65 illuminant and the CIE - * 2° Standard Observer (1931).

- * - *
    - *
  • outXyz[0] is X [0 ...95.047)
  • - *
  • outXyz[1] is Y [0...100)
  • - *
  • outXyz[2] is Z [0...108.883)
  • - *
- * - * @param r red component value [0..255] - * @param g green component value [0..255] - * @param b blue component value [0..255] - * @param outXyz 3-element array which holds the resulting XYZ components - */ - public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, - @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, - @NonNull double[] outXyz) { - if (outXyz.length != 3) { - throw new IllegalArgumentException("outXyz must have a length of 3."); - } - - double sr = r / 255.0; - sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); - double sg = g / 255.0; - sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); - double sb = b / 255.0; - sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); - - outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); - outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); - outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); - } - - /** - * Converts a color from CIE XYZ to CIE Lab representation. - * - *

This method expects the XYZ representation to use the D65 illuminant and the CIE - * 2° Standard Observer (1931).

- * - *
    - *
  • outLab[0] is L [0 ...100)
  • - *
  • outLab[1] is a [-128...127)
  • - *
  • outLab[2] is b [-128...127)
  • - *
- * - * @param x X component value [0...95.047) - * @param y Y component value [0...100) - * @param z Z component value [0...108.883) - * @param outLab 3-element array which holds the resulting Lab components - */ - public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, - @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, - @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, - @NonNull double[] outLab) { - if (outLab.length != 3) { - throw new IllegalArgumentException("outLab must have a length of 3."); - } - x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); - y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); - z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); - outLab[0] = Math.max(0, 116 * y - 16); - outLab[1] = 500 * (x - y); - outLab[2] = 200 * (y - z); - } - - /** - * Converts a color from CIE Lab to CIE XYZ representation. - * - *

The resulting XYZ representation will use the D65 illuminant and the CIE - * 2° Standard Observer (1931).

- * - *
    - *
  • outXyz[0] is X [0 ...95.047)
  • - *
  • outXyz[1] is Y [0...100)
  • - *
  • outXyz[2] is Z [0...108.883)
  • - *
- * - * @param l L component value [0...100) - * @param a A component value [-128...127) - * @param b B component value [-128...127) - * @param outXyz 3-element array which holds the resulting XYZ components - */ - public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, - @FloatRange(from = -128, to = 127) final double a, - @FloatRange(from = -128, to = 127) final double b, - @NonNull double[] outXyz) { - final double fy = (l + 16) / 116; - final double fx = a / 500 + fy; - final double fz = fy - b / 200; - - double tmp = Math.pow(fx, 3); - final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; - final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; - - tmp = Math.pow(fz, 3); - final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; - - outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; - outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; - outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; - } - - /** - * Converts a color from CIE XYZ to its RGB representation. - * - *

This method expects the XYZ representation to use the D65 illuminant and the CIE - * 2° Standard Observer (1931).

- * - * @param x X component value [0...95.047) - * @param y Y component value [0...100) - * @param z Z component value [0...108.883) - * @return int containing the RGB representation - */ - @ColorInt - public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, - @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, - @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { - double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; - double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; - double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; - - r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; - g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; - b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; - - return Color.rgb( - constrain((int) Math.round(r * 255), 0, 255), - constrain((int) Math.round(g * 255), 0, 255), - constrain((int) Math.round(b * 255), 0, 255)); - } - - /** - * Converts a color from CIE Lab to its RGB representation. - * - * @param l L component value [0...100] - * @param a A component value [-128...127] - * @param b B component value [-128...127] - * @return int containing the RGB representation - */ - @ColorInt - public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, - @FloatRange(from = -128, to = 127) final double a, - @FloatRange(from = -128, to = 127) final double b) { - final double[] result = getTempDouble3Array(); - LABToXYZ(l, a, b, result); - return XYZToColor(result[0], result[1], result[2]); - } - - private static int constrain(int amount, int low, int high) { - return amount < low ? low : (amount > high ? high : amount); - } - - private static float constrain(float amount, float low, float high) { - return amount < low ? low : (amount > high ? high : amount); - } - - private static double pivotXyzComponent(double component) { - return component > XYZ_EPSILON - ? Math.pow(component, 1 / 3.0) - : (XYZ_KAPPA * component + 16) / 116; - } - - public static double[] getTempDouble3Array() { - double[] result = TEMP_ARRAY.get(); - if (result == null) { - result = new double[3]; - TEMP_ARRAY.set(result); - } - return result; - } - - /** - * Convert HSL (hue-saturation-lightness) components to a RGB color. - *
    - *
  • hsl[0] is Hue [0 .. 360)
  • - *
  • hsl[1] is Saturation [0...1]
  • - *
  • hsl[2] is Lightness [0...1]
  • - *
- * If hsv values are out of range, they are pinned. - * - * @param hsl 3-element array which holds the input HSL components - * @return the resulting RGB color - */ - @ColorInt - public static int HSLToColor(@NonNull float[] hsl) { - final float h = hsl[0]; - final float s = hsl[1]; - final float l = hsl[2]; - - final float c = (1f - Math.abs(2 * l - 1f)) * s; - final float m = l - 0.5f * c; - final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); - - final int hueSegment = (int) h / 60; - - int r = 0, g = 0, b = 0; - - switch (hueSegment) { - case 0: - r = Math.round(255 * (c + m)); - g = Math.round(255 * (x + m)); - b = Math.round(255 * m); - break; - case 1: - r = Math.round(255 * (x + m)); - g = Math.round(255 * (c + m)); - b = Math.round(255 * m); - break; - case 2: - r = Math.round(255 * m); - g = Math.round(255 * (c + m)); - b = Math.round(255 * (x + m)); - break; - case 3: - r = Math.round(255 * m); - g = Math.round(255 * (x + m)); - b = Math.round(255 * (c + m)); - break; - case 4: - r = Math.round(255 * (x + m)); - g = Math.round(255 * m); - b = Math.round(255 * (c + m)); - break; - case 5: - case 6: - r = Math.round(255 * (c + m)); - g = Math.round(255 * m); - b = Math.round(255 * (x + m)); - break; - } - - r = constrain(r, 0, 255); - g = constrain(g, 0, 255); - b = constrain(b, 0, 255); - - return Color.rgb(r, g, b); - } - - /** - * Convert the ARGB color to its HSL (hue-saturation-lightness) components. - *
    - *
  • outHsl[0] is Hue [0 .. 360)
  • - *
  • outHsl[1] is Saturation [0...1]
  • - *
  • outHsl[2] is Lightness [0...1]
  • - *
- * - * @param color the ARGB color to convert. The alpha component is ignored - * @param outHsl 3-element array which holds the resulting HSL components - */ - public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { - RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); - } - - /** - * Convert RGB components to HSL (hue-saturation-lightness). - *
    - *
  • outHsl[0] is Hue [0 .. 360)
  • - *
  • outHsl[1] is Saturation [0...1]
  • - *
  • outHsl[2] is Lightness [0...1]
  • - *
- * - * @param r red component value [0..255] - * @param g green component value [0..255] - * @param b blue component value [0..255] - * @param outHsl 3-element array which holds the resulting HSL components - */ - public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r, - @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, - @NonNull float[] outHsl) { - final float rf = r / 255f; - final float gf = g / 255f; - final float bf = b / 255f; - - final float max = Math.max(rf, Math.max(gf, bf)); - final float min = Math.min(rf, Math.min(gf, bf)); - final float deltaMaxMin = max - min; - - float h, s; - float l = (max + min) / 2f; - - if (max == min) { - // Monochromatic - h = s = 0f; - } else { - if (max == rf) { - h = ((gf - bf) / deltaMaxMin) % 6f; - } else if (max == gf) { - h = ((bf - rf) / deltaMaxMin) + 2f; - } else { - h = ((rf - gf) / deltaMaxMin) + 4f; - } - - s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); - } - - h = (h * 60f) % 360f; - if (h < 0) { - h += 360f; - } - - outHsl[0] = constrain(h, 0f, 360f); - outHsl[1] = constrain(s, 0f, 1f); - outHsl[2] = constrain(l, 0f, 1f); - } - + public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { + RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); } -} \ No newline at end of file + + /** + * Convert RGB components to its CIE Lab representative components. + * + *
    + *
  • outLab[0] is L [0 ...100) + *
  • outLab[1] is a [-128...127) + *
  • outLab[2] is b [-128...127) + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outLab 3-element array which holds the resulting LAB components + */ + public static void RGBToLAB( + @IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, + @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull double[] outLab) { + // First we convert RGB to XYZ + RGBToXYZ(r, g, b, outLab); + // outLab now contains XYZ + XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); + // outLab now contains LAB representation + } + + /** + * Convert the ARGB color to it's CIE XYZ representative components. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard + * Observer (1931). + * + *

    + *
  • outXyz[0] is X [0 ...95.047) + *
  • outXyz[1] is Y [0...100) + *
  • outXyz[2] is Z [0...108.883) + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outXyz 3-element array which holds the resulting LAB components + */ + public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { + RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); + } + + /** + * Convert RGB components to it's CIE XYZ representative components. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard + * Observer (1931). + * + *

    + *
  • outXyz[0] is X [0 ...95.047) + *
  • outXyz[1] is Y [0...100) + *
  • outXyz[2] is Z [0...108.883) + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outXyz 3-element array which holds the resulting XYZ components + */ + public static void RGBToXYZ( + @IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, + @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull double[] outXyz) { + if (outXyz.length != 3) { + throw new IllegalArgumentException("outXyz must have a length of 3."); + } + + double sr = r / 255.0; + sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); + double sg = g / 255.0; + sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); + double sb = b / 255.0; + sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); + + outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); + outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); + outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); + } + + /** + * Converts a color from CIE XYZ to CIE Lab representation. + * + *

This method expects the XYZ representation to use the D65 illuminant and the CIE 2° + * Standard Observer (1931). + * + *

    + *
  • outLab[0] is L [0 ...100) + *
  • outLab[1] is a [-128...127) + *
  • outLab[2] is b [-128...127) + *
+ * + * @param x X component value [0...95.047) + * @param y Y component value [0...100) + * @param z Z component value [0...108.883) + * @param outLab 3-element array which holds the resulting Lab components + */ + public static void XYZToLAB( + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, + @NonNull double[] outLab) { + if (outLab.length != 3) { + throw new IllegalArgumentException("outLab must have a length of 3."); + } + x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); + y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); + z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); + outLab[0] = Math.max(0, 116 * y - 16); + outLab[1] = 500 * (x - y); + outLab[2] = 200 * (y - z); + } + + /** + * Converts a color from CIE Lab to CIE XYZ representation. + * + *

The resulting XYZ representation will use the D65 illuminant and the CIE 2° Standard + * Observer (1931). + * + *

    + *
  • outXyz[0] is X [0 ...95.047) + *
  • outXyz[1] is Y [0...100) + *
  • outXyz[2] is Z [0...108.883) + *
+ * + * @param l L component value [0...100) + * @param a A component value [-128...127) + * @param b B component value [-128...127) + * @param outXyz 3-element array which holds the resulting XYZ components + */ + public static void LABToXYZ( + @FloatRange(from = 0f, to = 100) final double l, + @FloatRange(from = -128, to = 127) final double a, + @FloatRange(from = -128, to = 127) final double b, + @NonNull double[] outXyz) { + final double fy = (l + 16) / 116; + final double fx = a / 500 + fy; + final double fz = fy - b / 200; + + double tmp = Math.pow(fx, 3); + final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; + final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; + + tmp = Math.pow(fz, 3); + final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; + + outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; + outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; + outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; + } + + /** + * Converts a color from CIE XYZ to its RGB representation. + * + *

This method expects the XYZ representation to use the D65 illuminant and the CIE 2° + * Standard Observer (1931). + * + * @param x X component value [0...95.047) + * @param y Y component value [0...100) + * @param z Z component value [0...108.883) + * @return int containing the RGB representation + */ + @ColorInt + public static int XYZToColor( + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, + @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { + double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; + double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; + double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; + + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; + b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; + + return Color.rgb( + constrain((int) Math.round(r * 255), 0, 255), + constrain((int) Math.round(g * 255), 0, 255), + constrain((int) Math.round(b * 255), 0, 255)); + } + + /** + * Converts a color from CIE Lab to its RGB representation. + * + * @param l L component value [0...100] + * @param a A component value [-128...127] + * @param b B component value [-128...127] + * @return int containing the RGB representation + */ + @ColorInt + public static int LABToColor( + @FloatRange(from = 0f, to = 100) final double l, + @FloatRange(from = -128, to = 127) final double a, + @FloatRange(from = -128, to = 127) final double b) { + final double[] result = getTempDouble3Array(); + LABToXYZ(l, a, b, result); + return XYZToColor(result[0], result[1], result[2]); + } + + private static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static double pivotXyzComponent(double component) { + return component > XYZ_EPSILON + ? Math.pow(component, 1 / 3.0) + : (XYZ_KAPPA * component + 16) / 116; + } + + public static double[] getTempDouble3Array() { + double[] result = TEMP_ARRAY.get(); + if (result == null) { + result = new double[3]; + TEMP_ARRAY.set(result); + } + return result; + } + + /** + * Convert HSL (hue-saturation-lightness) components to a RGB color. + * + *

    + *
  • hsl[0] is Hue [0 .. 360) + *
  • hsl[1] is Saturation [0...1] + *
  • hsl[2] is Lightness [0...1] + *
+ * + * If hsv values are out of range, they are pinned. + * + * @param hsl 3-element array which holds the input HSL components + * @return the resulting RGB color + */ + @ColorInt + public static int HSLToColor(@NonNull float[] hsl) { + final float h = hsl[0]; + final float s = hsl[1]; + final float l = hsl[2]; + + final float c = (1f - Math.abs(2 * l - 1f)) * s; + final float m = l - 0.5f * c; + final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); + + final int hueSegment = (int) h / 60; + + int r = 0, g = 0, b = 0; + + switch (hueSegment) { + case 0: + r = Math.round(255 * (c + m)); + g = Math.round(255 * (x + m)); + b = Math.round(255 * m); + break; + case 1: + r = Math.round(255 * (x + m)); + g = Math.round(255 * (c + m)); + b = Math.round(255 * m); + break; + case 2: + r = Math.round(255 * m); + g = Math.round(255 * (c + m)); + b = Math.round(255 * (x + m)); + break; + case 3: + r = Math.round(255 * m); + g = Math.round(255 * (x + m)); + b = Math.round(255 * (c + m)); + break; + case 4: + r = Math.round(255 * (x + m)); + g = Math.round(255 * m); + b = Math.round(255 * (c + m)); + break; + case 5: + case 6: + r = Math.round(255 * (c + m)); + g = Math.round(255 * m); + b = Math.round(255 * (x + m)); + break; + } + + r = constrain(r, 0, 255); + g = constrain(g, 0, 255); + b = constrain(b, 0, 255); + + return Color.rgb(r, g, b); + } + + /** + * Convert the ARGB color to its HSL (hue-saturation-lightness) components. + * + *
    + *
  • outHsl[0] is Hue [0 .. 360) + *
  • outHsl[1] is Saturation [0...1] + *
  • outHsl[2] is Lightness [0...1] + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored + * @param outHsl 3-element array which holds the resulting HSL components + */ + public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { + RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); + } + + /** + * Convert RGB components to HSL (hue-saturation-lightness). + * + *
    + *
  • outHsl[0] is Hue [0 .. 360) + *
  • outHsl[1] is Saturation [0...1] + *
  • outHsl[2] is Lightness [0...1] + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param outHsl 3-element array which holds the resulting HSL components + */ + public static void RGBToHSL( + @IntRange(from = 0x0, to = 0xFF) int r, + @IntRange(from = 0x0, to = 0xFF) int g, + @IntRange(from = 0x0, to = 0xFF) int b, + @NonNull float[] outHsl) { + final float rf = r / 255f; + final float gf = g / 255f; + final float bf = b / 255f; + + final float max = Math.max(rf, Math.max(gf, bf)); + final float min = Math.min(rf, Math.min(gf, bf)); + final float deltaMaxMin = max - min; + + float h, s; + float l = (max + min) / 2f; + + if (max == min) { + // Monochromatic + h = s = 0f; + } else { + if (max == rf) { + h = ((gf - bf) / deltaMaxMin) % 6f; + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2f; + } else { + h = ((rf - gf) / deltaMaxMin) + 4f; + } + + s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); + } + + h = (h * 60f) % 360f; + if (h < 0) { + h += 360f; + } + + outHsl[0] = constrain(h, 0f, 360f); + outHsl[1] = constrain(s, 0f, 1f); + outHsl[2] = constrain(l, 0f, 1f); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/BaselineGridTextView.java b/app/src/main/java/code/name/monkey/retromusic/views/BaselineGridTextView.java index 8cce44b15..711094db2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/BaselineGridTextView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/BaselineGridTextView.java @@ -19,184 +19,173 @@ import android.content.res.TypedArray; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; - import androidx.annotation.FontRes; - -import com.google.android.material.textview.MaterialTextView; - import code.name.monkey.retromusic.R; +import com.google.android.material.textview.MaterialTextView; public class BaselineGridTextView extends MaterialTextView { - private final float FOUR_DIP; + private final float FOUR_DIP; - private int extraBottomPadding = 0; + private int extraBottomPadding = 0; - private int extraTopPadding = 0; + private int extraTopPadding = 0; - private @FontRes - int fontResId = 0; + private @FontRes int fontResId = 0; - private float lineHeightHint = 0f; + private float lineHeightHint = 0f; - private float lineHeightMultiplierHint = 1f; + private float lineHeightMultiplierHint = 1f; - private boolean maxLinesByHeight = false; + private boolean maxLinesByHeight = false; - public BaselineGridTextView(Context context) { - this(context, null); + public BaselineGridTextView(Context context) { + this(context, null); + } + + public BaselineGridTextView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public BaselineGridTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.BaselineGridTextView, defStyleAttr, 0); + + // first check TextAppearance for line height & font attributes + if (a.hasValue(R.styleable.BaselineGridTextView_android_textAppearance)) { + int textAppearanceId = + a.getResourceId( + R.styleable.BaselineGridTextView_android_textAppearance, + android.R.style.TextAppearance); + TypedArray ta = + context.obtainStyledAttributes(textAppearanceId, R.styleable.BaselineGridTextView); + parseTextAttrs(ta); + ta.recycle(); } - public BaselineGridTextView(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.textViewStyle); + // then check view attrs + parseTextAttrs(a); + maxLinesByHeight = a.getBoolean(R.styleable.BaselineGridTextView_maxLinesByHeight, false); + a.recycle(); + + FOUR_DIP = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics()); + computeLineHeight(); + } + + @Override + public int getCompoundPaddingBottom() { + // include extra padding to make the height a multiple of 4dp + return super.getCompoundPaddingBottom() + extraBottomPadding; + } + + @Override + public int getCompoundPaddingTop() { + // include extra padding to place the first line's baseline on the grid + return super.getCompoundPaddingTop() + extraTopPadding; + } + + public @FontRes int getFontResId() { + return fontResId; + } + + public float getLineHeightHint() { + return lineHeightHint; + } + + public void setLineHeightHint(float lineHeightHint) { + this.lineHeightHint = lineHeightHint; + computeLineHeight(); + } + + public float getLineHeightMultiplierHint() { + return lineHeightMultiplierHint; + } + + public void setLineHeightMultiplierHint(float lineHeightMultiplierHint) { + this.lineHeightMultiplierHint = lineHeightMultiplierHint; + computeLineHeight(); + } + + public boolean getMaxLinesByHeight() { + return maxLinesByHeight; + } + + public void setMaxLinesByHeight(boolean maxLinesByHeight) { + this.maxLinesByHeight = maxLinesByHeight; + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + extraTopPadding = 0; + extraBottomPadding = 0; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int height = getMeasuredHeight(); + height += ensureBaselineOnGrid(); + height += ensureHeightGridAligned(height); + setMeasuredDimension(getMeasuredWidth(), height); + checkMaxLines(height, MeasureSpec.getMode(heightMeasureSpec)); + } + + /** + * When measured with an exact height, text can be vertically clipped mid-line. Prevent this by + * setting the {@code maxLines} property based on the available space. + */ + private void checkMaxLines(int height, int heightMode) { + if (!maxLinesByHeight || heightMode != MeasureSpec.EXACTLY) { + return; } - public BaselineGridTextView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + int textHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); + int completeLines = (int) Math.floor(textHeight / getLineHeight()); + setMaxLines(completeLines); + } - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.BaselineGridTextView, defStyleAttr, 0); + /** Ensures line height is a multiple of 4dp. */ + private void computeLineHeight() { + final Paint.FontMetrics fm = getPaint().getFontMetrics(); + final float fontHeight = Math.abs(fm.ascent - fm.descent) + fm.leading; + final float desiredLineHeight = + (lineHeightHint > 0) ? lineHeightHint : lineHeightMultiplierHint * fontHeight; - // first check TextAppearance for line height & font attributes - if (a.hasValue(R.styleable.BaselineGridTextView_android_textAppearance)) { - int textAppearanceId = - a.getResourceId(R.styleable.BaselineGridTextView_android_textAppearance, - android.R.style.TextAppearance); - TypedArray ta = context.obtainStyledAttributes( - textAppearanceId, R.styleable.BaselineGridTextView); - parseTextAttrs(ta); - ta.recycle(); - } + final int baselineAlignedLineHeight = + (int) ((FOUR_DIP * (float) Math.ceil(desiredLineHeight / FOUR_DIP)) + 0.5f); + setLineSpacing(baselineAlignedLineHeight - fontHeight, 1f); + } - // then check view attrs - parseTextAttrs(a); - maxLinesByHeight = a.getBoolean(R.styleable.BaselineGridTextView_maxLinesByHeight, false); - a.recycle(); - - FOUR_DIP = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics()); - computeLineHeight(); + /** Ensure that the first line of text sits on the 4dp grid. */ + private int ensureBaselineOnGrid() { + float baseline = getBaseline(); + float gridAlign = baseline % FOUR_DIP; + if (gridAlign != 0) { + extraTopPadding = (int) (FOUR_DIP - Math.ceil(gridAlign)); } + return extraTopPadding; + } - @Override - public int getCompoundPaddingBottom() { - // include extra padding to make the height a multiple of 4dp - return super.getCompoundPaddingBottom() + extraBottomPadding; + /** Ensure that height is a multiple of 4dp. */ + private int ensureHeightGridAligned(int height) { + float gridOverhang = height % FOUR_DIP; + if (gridOverhang != 0) { + extraBottomPadding = (int) (FOUR_DIP - Math.ceil(gridOverhang)); } + return extraBottomPadding; + } - @Override - public int getCompoundPaddingTop() { - // include extra padding to place the first line's baseline on the grid - return super.getCompoundPaddingTop() + extraTopPadding; + private void parseTextAttrs(TypedArray a) { + if (a.hasValue(R.styleable.BaselineGridTextView_lineHeightMultiplierHint)) { + lineHeightMultiplierHint = + a.getFloat(R.styleable.BaselineGridTextView_lineHeightMultiplierHint, 1f); } - - public @FontRes - int getFontResId() { - return fontResId; + if (a.hasValue(R.styleable.BaselineGridTextView_lineHeightHint)) { + lineHeightHint = a.getDimensionPixelSize(R.styleable.BaselineGridTextView_lineHeightHint, 0); } - - public float getLineHeightHint() { - return lineHeightHint; + if (a.hasValue(R.styleable.BaselineGridTextView_android_fontFamily)) { + fontResId = a.getResourceId(R.styleable.BaselineGridTextView_android_fontFamily, 0); } - - public void setLineHeightHint(float lineHeightHint) { - this.lineHeightHint = lineHeightHint; - computeLineHeight(); - } - - public float getLineHeightMultiplierHint() { - return lineHeightMultiplierHint; - } - - public void setLineHeightMultiplierHint(float lineHeightMultiplierHint) { - this.lineHeightMultiplierHint = lineHeightMultiplierHint; - computeLineHeight(); - } - - public boolean getMaxLinesByHeight() { - return maxLinesByHeight; - } - - public void setMaxLinesByHeight(boolean maxLinesByHeight) { - this.maxLinesByHeight = maxLinesByHeight; - requestLayout(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - extraTopPadding = 0; - extraBottomPadding = 0; - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int height = getMeasuredHeight(); - height += ensureBaselineOnGrid(); - height += ensureHeightGridAligned(height); - setMeasuredDimension(getMeasuredWidth(), height); - checkMaxLines(height, MeasureSpec.getMode(heightMeasureSpec)); - } - - /** - * When measured with an exact height, text can be vertically clipped mid-line. Prevent - * this by setting the {@code maxLines} property based on the available space. - */ - private void checkMaxLines(int height, int heightMode) { - if (!maxLinesByHeight || heightMode != MeasureSpec.EXACTLY) { - return; - } - - int textHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); - int completeLines = (int) Math.floor(textHeight / getLineHeight()); - setMaxLines(completeLines); - } - - /** - * Ensures line height is a multiple of 4dp. - */ - private void computeLineHeight() { - final Paint.FontMetrics fm = getPaint().getFontMetrics(); - final float fontHeight = Math.abs(fm.ascent - fm.descent) + fm.leading; - final float desiredLineHeight = (lineHeightHint > 0) - ? lineHeightHint - : lineHeightMultiplierHint * fontHeight; - - final int baselineAlignedLineHeight = - (int) ((FOUR_DIP * (float) Math.ceil(desiredLineHeight / FOUR_DIP)) + 0.5f); - setLineSpacing(baselineAlignedLineHeight - fontHeight, 1f); - } - - /** - * Ensure that the first line of text sits on the 4dp grid. - */ - private int ensureBaselineOnGrid() { - float baseline = getBaseline(); - float gridAlign = baseline % FOUR_DIP; - if (gridAlign != 0) { - extraTopPadding = (int) (FOUR_DIP - Math.ceil(gridAlign)); - } - return extraTopPadding; - } - - /** - * Ensure that height is a multiple of 4dp. - */ - private int ensureHeightGridAligned(int height) { - float gridOverhang = height % FOUR_DIP; - if (gridOverhang != 0) { - extraBottomPadding = (int) (FOUR_DIP - Math.ceil(gridOverhang)); - } - return extraBottomPadding; - } - - private void parseTextAttrs(TypedArray a) { - if (a.hasValue(R.styleable.BaselineGridTextView_lineHeightMultiplierHint)) { - lineHeightMultiplierHint = - a.getFloat(R.styleable.BaselineGridTextView_lineHeightMultiplierHint, 1f); - } - if (a.hasValue(R.styleable.BaselineGridTextView_lineHeightHint)) { - lineHeightHint = a.getDimensionPixelSize( - R.styleable.BaselineGridTextView_lineHeightHint, 0); - } - if (a.hasValue(R.styleable.BaselineGridTextView_android_fontFamily)) { - fontResId = a.getResourceId(R.styleable.BaselineGridTextView_android_fontFamily, 0); - } - } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/BreadCrumbLayout.java b/app/src/main/java/code/name/monkey/retromusic/views/BreadCrumbLayout.java index 944ca9c1f..c13802b19 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/BreadCrumbLayout.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/BreadCrumbLayout.java @@ -26,422 +26,429 @@ import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; - +import code.name.monkey.appthemehelper.util.ATHUtil; +import code.name.monkey.retromusic.R; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; -import code.name.monkey.appthemehelper.util.ATHUtil; -import code.name.monkey.retromusic.R; - -/** - * @author Aidan Follestad (afollestad), modified for Phonograph by Karim Abou Zeid (kabouzeid) - */ +/** @author Aidan Follestad (afollestad), modified for Phonograph by Karim Abou Zeid (kabouzeid) */ public class BreadCrumbLayout extends HorizontalScrollView implements View.OnClickListener { - @ColorInt - private int contentColorActivated; - @ColorInt - private int contentColorDeactivated; - private int mActive; - private SelectionCallback mCallback; - private LinearLayout mChildFrame; - // Stores currently visible crumbs - private List mCrumbs; - // Stores user's navigation history, like a fragment back stack - private List mHistory; - // Used in setActiveOrAdd() between clearing crumbs and adding the new set, nullified afterwards - private List mOldCrumbs; + @ColorInt private int contentColorActivated; + @ColorInt private int contentColorDeactivated; + private int mActive; + private SelectionCallback mCallback; + private LinearLayout mChildFrame; + // Stores currently visible crumbs + private List mCrumbs; + // Stores user's navigation history, like a fragment back stack + private List mHistory; + // Used in setActiveOrAdd() between clearing crumbs and adding the new set, nullified afterwards + private List mOldCrumbs; - public BreadCrumbLayout(Context context) { - super(context); - init(); + public BreadCrumbLayout(Context context) { + super(context); + init(); + } + + public BreadCrumbLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BreadCrumbLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public void addCrumb(@NonNull Crumb crumb, boolean refreshLayout) { + LinearLayout view = + (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.bread_crumb, this, false); + view.setTag(mCrumbs.size()); + view.setOnClickListener(this); + + ImageView iv = (ImageView) view.getChildAt(1); + if (iv.getDrawable() != null) { + iv.getDrawable().setAutoMirrored(true); } + iv.setVisibility(View.GONE); - public BreadCrumbLayout(Context context, AttributeSet attrs) { - super(context, attrs); - init(); + mChildFrame.addView( + view, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + mCrumbs.add(crumb); + if (refreshLayout) { + mActive = mCrumbs.size() - 1; + requestLayout(); } + invalidateActivatedAll(); + } - public BreadCrumbLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); + public void addHistory(Crumb crumb) { + mHistory.add(crumb); + } + + public void clearCrumbs() { + try { + mOldCrumbs = new ArrayList<>(mCrumbs); + mCrumbs.clear(); + mChildFrame.removeAllViews(); + } catch (IllegalStateException e) { + e.printStackTrace(); } + } - public void addCrumb(@NonNull Crumb crumb, boolean refreshLayout) { - LinearLayout view = (LinearLayout) LayoutInflater.from(getContext()) - .inflate(R.layout.bread_crumb, this, false); - view.setTag(mCrumbs.size()); - view.setOnClickListener(this); + public void clearHistory() { + mHistory.clear(); + } - ImageView iv = (ImageView) view.getChildAt(1); - if (iv.getDrawable() != null) { - iv.getDrawable().setAutoMirrored(true); - } - iv.setVisibility(View.GONE); - - mChildFrame.addView(view, new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - mCrumbs.add(crumb); - if (refreshLayout) { - mActive = mCrumbs.size() - 1; - requestLayout(); - } - invalidateActivatedAll(); + public Crumb findCrumb(@NonNull File forDir) { + for (int i = 0; i < mCrumbs.size(); i++) { + if (mCrumbs.get(i).getFile().equals(forDir)) { + return mCrumbs.get(i); + } } + return null; + } - public void addHistory(Crumb crumb) { - mHistory.add(crumb); + public int getActiveIndex() { + return mActive; + } + + public Crumb getCrumb(int index) { + return mCrumbs.get(index); + } + + public SavedStateWrapper getStateWrapper() { + return new SavedStateWrapper(this); + } + + public int historySize() { + return mHistory.size(); + } + + public Crumb lastHistory() { + if (mHistory.size() == 0) { + return null; } + return mHistory.get(mHistory.size() - 1); + } - public void clearCrumbs() { - try { - mOldCrumbs = new ArrayList<>(mCrumbs); - mCrumbs.clear(); - mChildFrame.removeAllViews(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } + @Override + public void onClick(View v) { + if (mCallback != null) { + int index = (Integer) v.getTag(); + mCallback.onCrumbSelection(mCrumbs.get(index), index); } + } - public void clearHistory() { - mHistory.clear(); + public boolean popHistory() { + if (mHistory.size() == 0) { + return false; } + mHistory.remove(mHistory.size() - 1); + return mHistory.size() != 0; + } - public Crumb findCrumb(@NonNull File forDir) { - for (int i = 0; i < mCrumbs.size(); i++) { - if (mCrumbs.get(i).getFile().equals(forDir)) { - return mCrumbs.get(i); + public void restoreFromStateWrapper(SavedStateWrapper mSavedState) { + if (mSavedState != null) { + mActive = mSavedState.mActive; + for (Crumb c : mSavedState.mCrumbs) { + addCrumb(c, false); + } + requestLayout(); + setVisibility(mSavedState.mVisibility); + } + } + + public void reverseHistory() { + Collections.reverse(mHistory); + } + + public void setActivatedContentColor(@ColorInt int contentColorActivated) { + this.contentColorActivated = contentColorActivated; + } + + public void setActiveOrAdd(@NonNull Crumb crumb, boolean forceRecreate) { + if (forceRecreate || !setActive(crumb)) { + clearCrumbs(); + final List newPathSet = new ArrayList<>(); + + newPathSet.add(0, crumb.getFile()); + + File p = crumb.getFile(); + while ((p = p.getParentFile()) != null) { + newPathSet.add(0, p); + } + + for (int index = 0; index < newPathSet.size(); index++) { + final File fi = newPathSet.get(index); + crumb = new Crumb(fi); + + // Restore scroll positions saved before clearing + if (mOldCrumbs != null) { + for (Iterator iterator = mOldCrumbs.iterator(); iterator.hasNext(); ) { + Crumb old = iterator.next(); + if (old.equals(crumb)) { + crumb.setScrollPosition(old.getScrollPosition()); + iterator.remove(); // minimize number of linear passes by removing un-used crumbs from + // history + break; } + } } - return null; + + addCrumb(crumb, true); + } + + // History no longer needed + mOldCrumbs = null; + } + } + + public void setCallback(SelectionCallback callback) { + mCallback = callback; + } + + public void setDeactivatedContentColor(@ColorInt int contentColorDeactivated) { + this.contentColorDeactivated = contentColorDeactivated; + } + + public int size() { + return mCrumbs.size(); + } + + public boolean trim(String path, boolean dir) { + if (!dir) { + return false; + } + int index = -1; + for (int i = mCrumbs.size() - 1; i >= 0; i--) { + File fi = mCrumbs.get(i).getFile(); + if (fi.getPath().equals(path)) { + index = i; + break; + } } - public int getActiveIndex() { - return mActive; + boolean removedActive = index >= mActive; + if (index > -1) { + while (index <= mCrumbs.size() - 1) { + removeCrumbAt(index); + } + if (mChildFrame.getChildCount() > 0) { + int lastIndex = mCrumbs.size() - 1; + invalidateActivated(mChildFrame.getChildAt(lastIndex), mActive == lastIndex, false, false); + } + } + return removedActive || mCrumbs.size() == 0; + } + + public boolean trim(File file) { + return trim(file.getPath(), file.isDirectory()); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + // RTL works fine like this + View child = mChildFrame.getChildAt(mActive); + if (child != null) { + smoothScrollTo(child.getLeft(), 0); + } + } + + void invalidateActivatedAll() { + for (int i = 0; i < mCrumbs.size(); i++) { + Crumb crumb = mCrumbs.get(i); + invalidateActivated( + mChildFrame.getChildAt(i), + mActive == mCrumbs.indexOf(crumb), + false, + i < mCrumbs.size() - 1) + .setText(crumb.getTitle()); + } + } + + void removeCrumbAt(int index) { + mCrumbs.remove(index); + mChildFrame.removeViewAt(index); + } + + void updateIndices() { + for (int i = 0; i < mChildFrame.getChildCount(); i++) { + mChildFrame.getChildAt(i).setTag(i); + } + } + + private void init() { + contentColorActivated = + ATHUtil.INSTANCE.resolveColor(getContext(), android.R.attr.textColorPrimary); + contentColorDeactivated = + ATHUtil.INSTANCE.resolveColor(getContext(), android.R.attr.textColorSecondary); + setMinimumHeight((int) getResources().getDimension(R.dimen.tab_height)); + setClipToPadding(false); + setHorizontalScrollBarEnabled(false); + mCrumbs = new ArrayList<>(); + mHistory = new ArrayList<>(); + mChildFrame = new LinearLayout(getContext()); + addView( + mChildFrame, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + + private TextView invalidateActivated( + View view, + final boolean isActive, + final boolean noArrowIfAlone, + final boolean allowArrowVisible) { + int contentColor = isActive ? contentColorActivated : contentColorDeactivated; + LinearLayout child = (LinearLayout) view; + TextView tv = (TextView) child.getChildAt(0); + tv.setTextColor(contentColor); + ImageView iv = (ImageView) child.getChildAt(1); + iv.setColorFilter(contentColor, PorterDuff.Mode.SRC_IN); + if (noArrowIfAlone && getChildCount() == 1) { + iv.setVisibility(View.GONE); + } else if (allowArrowVisible) { + iv.setVisibility(View.VISIBLE); + } else { + iv.setVisibility(View.GONE); + } + return tv; + } + + private boolean setActive(Crumb newActive) { + mActive = mCrumbs.indexOf(newActive); + invalidateActivatedAll(); + boolean success = mActive > -1; + if (success) { + requestLayout(); + } + return success; + } + + public interface SelectionCallback { + + void onCrumbSelection(Crumb crumb, int index); + } + + public static class Crumb implements Parcelable { + + public static final Creator CREATOR = + new Creator() { + @Override + public Crumb createFromParcel(Parcel source) { + return new Crumb(source); + } + + @Override + public Crumb[] newArray(int size) { + return new Crumb[size]; + } + }; + + private final File file; + + private int scrollPos; + + public Crumb(File file) { + this.file = file; } - public Crumb getCrumb(int index) { - return mCrumbs.get(index); - } - - public SavedStateWrapper getStateWrapper() { - return new SavedStateWrapper(this); - } - - public int historySize() { - return mHistory.size(); - } - - public Crumb lastHistory() { - if (mHistory.size() == 0) { - return null; - } - return mHistory.get(mHistory.size() - 1); + protected Crumb(Parcel in) { + this.file = (File) in.readSerializable(); + this.scrollPos = in.readInt(); } @Override - public void onClick(View v) { - if (mCallback != null) { - int index = (Integer) v.getTag(); - mCallback.onCrumbSelection(mCrumbs.get(index), index); - } - } - - public boolean popHistory() { - if (mHistory.size() == 0) { - return false; - } - mHistory.remove(mHistory.size() - 1); - return mHistory.size() != 0; - } - - public void restoreFromStateWrapper(SavedStateWrapper mSavedState) { - if (mSavedState != null) { - mActive = mSavedState.mActive; - for (Crumb c : mSavedState.mCrumbs) { - addCrumb(c, false); - } - requestLayout(); - setVisibility(mSavedState.mVisibility); - } - } - - public void reverseHistory() { - Collections.reverse(mHistory); - } - - public void setActivatedContentColor(@ColorInt int contentColorActivated) { - this.contentColorActivated = contentColorActivated; - } - - public void setActiveOrAdd(@NonNull Crumb crumb, boolean forceRecreate) { - if (forceRecreate || !setActive(crumb)) { - clearCrumbs(); - final List newPathSet = new ArrayList<>(); - - newPathSet.add(0, crumb.getFile()); - - File p = crumb.getFile(); - while ((p = p.getParentFile()) != null) { - newPathSet.add(0, p); - } - - for (int index = 0; index < newPathSet.size(); index++) { - final File fi = newPathSet.get(index); - crumb = new Crumb(fi); - - // Restore scroll positions saved before clearing - if (mOldCrumbs != null) { - for (Iterator iterator = mOldCrumbs.iterator(); iterator.hasNext(); ) { - Crumb old = iterator.next(); - if (old.equals(crumb)) { - crumb.setScrollPosition(old.getScrollPosition()); - iterator.remove(); // minimize number of linear passes by removing un-used crumbs from history - break; - } - } - } - - addCrumb(crumb, true); - } - - // History no longer needed - mOldCrumbs = null; - } - } - - public void setCallback(SelectionCallback callback) { - mCallback = callback; - } - - public void setDeactivatedContentColor(@ColorInt int contentColorDeactivated) { - this.contentColorDeactivated = contentColorDeactivated; - } - - public int size() { - return mCrumbs.size(); - } - - public boolean trim(String path, boolean dir) { - if (!dir) { - return false; - } - int index = -1; - for (int i = mCrumbs.size() - 1; i >= 0; i--) { - File fi = mCrumbs.get(i).getFile(); - if (fi.getPath().equals(path)) { - index = i; - break; - } - } - - boolean removedActive = index >= mActive; - if (index > -1) { - while (index <= mCrumbs.size() - 1) { - removeCrumbAt(index); - } - if (mChildFrame.getChildCount() > 0) { - int lastIndex = mCrumbs.size() - 1; - invalidateActivated(mChildFrame.getChildAt(lastIndex), mActive == lastIndex, false, false); - } - } - return removedActive || mCrumbs.size() == 0; - } - - public boolean trim(File file) { - return trim(file.getPath(), file.isDirectory()); + public int describeContents() { + return 0; } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - //RTL works fine like this - View child = mChildFrame.getChildAt(mActive); - if (child != null) { - smoothScrollTo(child.getLeft(), 0); - } + public boolean equals(Object o) { + return (o instanceof Crumb) + && ((Crumb) o).getFile() != null + && ((Crumb) o).getFile().equals(getFile()); } - void invalidateActivatedAll() { - for (int i = 0; i < mCrumbs.size(); i++) { - Crumb crumb = mCrumbs.get(i); - invalidateActivated(mChildFrame.getChildAt(i), mActive == mCrumbs.indexOf(crumb), false, - i < mCrumbs.size() - 1).setText(crumb.getTitle()); - } + public File getFile() { + return file; } - void removeCrumbAt(int index) { - mCrumbs.remove(index); - mChildFrame.removeViewAt(index); + public int getScrollPosition() { + return scrollPos; } - void updateIndices() { - for (int i = 0; i < mChildFrame.getChildCount(); i++) { - mChildFrame.getChildAt(i).setTag(i); - } + public void setScrollPosition(int scrollY) { + this.scrollPos = scrollY; } - private void init() { - contentColorActivated = ATHUtil.INSTANCE.resolveColor(getContext(), android.R.attr.textColorPrimary); - contentColorDeactivated = ATHUtil.INSTANCE.resolveColor(getContext(), android.R.attr.textColorSecondary); - setMinimumHeight((int) getResources().getDimension(R.dimen.tab_height)); - setClipToPadding(false); - setHorizontalScrollBarEnabled(false); - mCrumbs = new ArrayList<>(); - mHistory = new ArrayList<>(); - mChildFrame = new LinearLayout(getContext()); - addView(mChildFrame, new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + public String getTitle() { + return file.getPath().equals("/") ? "root" : file.getName(); } - private TextView invalidateActivated(View view, final boolean isActive, final boolean noArrowIfAlone, - final boolean allowArrowVisible) { - int contentColor = isActive ? contentColorActivated : contentColorDeactivated; - LinearLayout child = (LinearLayout) view; - TextView tv = (TextView) child.getChildAt(0); - tv.setTextColor(contentColor); - ImageView iv = (ImageView) child.getChildAt(1); - iv.setColorFilter(contentColor, PorterDuff.Mode.SRC_IN); - if (noArrowIfAlone && getChildCount() == 1) { - iv.setVisibility(View.GONE); - } else if (allowArrowVisible) { - iv.setVisibility(View.VISIBLE); - } else { - iv.setVisibility(View.GONE); - } - return tv; + @Override + public String toString() { + return "Crumb{" + "file=" + file + ", scrollPos=" + scrollPos + '}'; } - private boolean setActive(Crumb newActive) { - mActive = mCrumbs.indexOf(newActive); - invalidateActivatedAll(); - boolean success = mActive > -1; - if (success) { - requestLayout(); - } - return success; + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeSerializable(this.file); + dest.writeInt(this.scrollPos); } + } - public interface SelectionCallback { + public static class SavedStateWrapper implements Parcelable { - void onCrumbSelection(Crumb crumb, int index); - } + public static final Creator CREATOR = + new Creator() { + public SavedStateWrapper createFromParcel(Parcel source) { + return new SavedStateWrapper(source); + } - public static class Crumb implements Parcelable { - - public static final Creator CREATOR = new Creator() { - @Override - public Crumb createFromParcel(Parcel source) { - return new Crumb(source); - } - - @Override - public Crumb[] newArray(int size) { - return new Crumb[size]; - } + public SavedStateWrapper[] newArray(int size) { + return new SavedStateWrapper[size]; + } }; - private final File file; + public final int mActive; - private int scrollPos; + public final List mCrumbs; - public Crumb(File file) { - this.file = file; - } + public final int mVisibility; - protected Crumb(Parcel in) { - this.file = (File) in.readSerializable(); - this.scrollPos = in.readInt(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public boolean equals(Object o) { - return (o instanceof Crumb) && ((Crumb) o).getFile() != null && - ((Crumb) o).getFile().equals(getFile()); - } - - public File getFile() { - return file; - } - - public int getScrollPosition() { - return scrollPos; - } - - public void setScrollPosition(int scrollY) { - this.scrollPos = scrollY; - } - - public String getTitle() { - return file.getPath().equals("/") ? "root" : file.getName(); - } - - @Override - public String toString() { - return "Crumb{" + - "file=" + file + - ", scrollPos=" + scrollPos + - '}'; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeSerializable(this.file); - dest.writeInt(this.scrollPos); - } + public SavedStateWrapper(BreadCrumbLayout view) { + mActive = view.mActive; + mCrumbs = view.mCrumbs; + mVisibility = view.getVisibility(); } - public static class SavedStateWrapper implements Parcelable { - - public static final Creator CREATOR = new Creator() { - public SavedStateWrapper createFromParcel(Parcel source) { - return new SavedStateWrapper(source); - } - - public SavedStateWrapper[] newArray(int size) { - return new SavedStateWrapper[size]; - } - }; - - public final int mActive; - - public final List mCrumbs; - - public final int mVisibility; - - public SavedStateWrapper(BreadCrumbLayout view) { - mActive = view.mActive; - mCrumbs = view.mCrumbs; - mVisibility = view.getVisibility(); - } - - protected SavedStateWrapper(Parcel in) { - this.mActive = in.readInt(); - this.mCrumbs = in.createTypedArrayList(Crumb.CREATOR); - this.mVisibility = in.readInt(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(this.mActive); - dest.writeTypedList(mCrumbs); - dest.writeInt(this.mVisibility); - } + protected SavedStateWrapper(Parcel in) { + this.mActive = in.readInt(); + this.mCrumbs = in.createTypedArrayList(Crumb.CREATOR); + this.mVisibility = in.readInt(); } -} \ No newline at end of file + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.mActive); + dest.writeTypedList(mCrumbs); + dest.writeInt(this.mVisibility); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/CircularImageView.java b/app/src/main/java/code/name/monkey/retromusic/views/CircularImageView.java index 3b555cf23..2c0dccad7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/CircularImageView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/CircularImageView.java @@ -27,299 +27,311 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; - import androidx.appcompat.widget.AppCompatImageView; - import code.name.monkey.retromusic.R; public class CircularImageView extends AppCompatImageView { - private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; + private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; - // Default Values - private static final float DEFAULT_BORDER_WIDTH = 4; - private static final float DEFAULT_SHADOW_RADIUS = 8.0f; + // Default Values + private static final float DEFAULT_BORDER_WIDTH = 4; + private static final float DEFAULT_SHADOW_RADIUS = 8.0f; - // Properties - private float borderWidth; - private int canvasSize; - private float shadowRadius; - private int shadowColor = Color.BLACK; + // Properties + private float borderWidth; + private int canvasSize; + private float shadowRadius; + private int shadowColor = Color.BLACK; - // Object used to draw - private Bitmap image; - private Drawable drawable; - private Paint paint; - private Paint paintBorder; + // Object used to draw + private Bitmap image; + private Drawable drawable; + private Paint paint; + private Paint paintBorder; - //region Constructor & Init Method - public CircularImageView(final Context context) { - this(context, null); + // region Constructor & Init Method + public CircularImageView(final Context context) { + this(context, null); + } + + public CircularImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + // Init paint + paint = new Paint(); + paint.setAntiAlias(true); + + paintBorder = new Paint(); + paintBorder.setAntiAlias(true); + + // Load the styled attributes and set their properties + TypedArray attributes = + context.obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyleAttr, 0); + + // Init Border + if (attributes.getBoolean(R.styleable.CircularImageView_civ_border, true)) { + float defaultBorderSize = + DEFAULT_BORDER_WIDTH * getContext().getResources().getDisplayMetrics().density; + setBorderWidth( + attributes.getDimension( + R.styleable.CircularImageView_civ_border_width, defaultBorderSize)); + setBorderColor( + attributes.getColor(R.styleable.CircularImageView_civ_border_color, Color.WHITE)); } - public CircularImageView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + // Init Shadow + if (attributes.getBoolean(R.styleable.CircularImageView_civ_shadow, false)) { + shadowRadius = DEFAULT_SHADOW_RADIUS; + drawShadow( + attributes.getFloat(R.styleable.CircularImageView_civ_shadow_radius, shadowRadius), + attributes.getColor(R.styleable.CircularImageView_civ_shadow_color, shadowColor)); + } + attributes.recycle(); + } + // endregion + + // region Set Attr Method + public void setBorderWidth(float borderWidth) { + this.borderWidth = borderWidth; + requestLayout(); + invalidate(); + } + + public void setBorderColor(int borderColor) { + if (paintBorder != null) { + paintBorder.setColor(borderColor); + } + invalidate(); + } + + public void addShadow() { + if (shadowRadius == 0) { + shadowRadius = DEFAULT_SHADOW_RADIUS; + } + drawShadow(shadowRadius, shadowColor); + invalidate(); + } + + public void setShadowRadius(float shadowRadius) { + drawShadow(shadowRadius, shadowColor); + invalidate(); + } + + public void setShadowColor(int shadowColor) { + drawShadow(shadowRadius, shadowColor); + invalidate(); + } + + @Override + public ScaleType getScaleType() { + return SCALE_TYPE; + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (scaleType != SCALE_TYPE) { + throw new IllegalArgumentException( + String.format( + "ScaleType %s not supported. ScaleType.CENTER_CROP is used by default. So you don't need to use ScaleType.", + scaleType)); + } + } + // endregion + + // region Draw Method + @Override + public void onDraw(Canvas canvas) { + // Load the bitmap + loadBitmap(); + + // Check if image isn't null + if (image == null) { + return; } - public CircularImageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr); + if (!isInEditMode()) { + canvasSize = canvas.getWidth(); + if (canvas.getHeight() < canvasSize) { + canvasSize = canvas.getHeight(); + } } - private void init(Context context, AttributeSet attrs, int defStyleAttr) { - // Init paint - paint = new Paint(); - paint.setAntiAlias(true); + // circleCenter is the x or y of the view's center + // radius is the radius in pixels of the cirle to be drawn + // paint contains the shader that will texture the shape + int circleCenter = (int) (canvasSize - (borderWidth * 2)) / 2; + // Draw Border + canvas.drawCircle( + circleCenter + borderWidth, + circleCenter + borderWidth, + circleCenter + borderWidth - (shadowRadius + shadowRadius / 2), + paintBorder); + // Draw CircularImageView + canvas.drawCircle( + circleCenter + borderWidth, + circleCenter + borderWidth, + circleCenter - (shadowRadius + shadowRadius / 2), + paint); + } - paintBorder = new Paint(); - paintBorder.setAntiAlias(true); - - // Load the styled attributes and set their properties - TypedArray attributes = context - .obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyleAttr, 0); - - // Init Border - if (attributes.getBoolean(R.styleable.CircularImageView_civ_border, true)) { - float defaultBorderSize = - DEFAULT_BORDER_WIDTH * getContext().getResources().getDisplayMetrics().density; - setBorderWidth(attributes - .getDimension(R.styleable.CircularImageView_civ_border_width, defaultBorderSize)); - setBorderColor( - attributes.getColor(R.styleable.CircularImageView_civ_border_color, Color.WHITE)); - } - - // Init Shadow - if (attributes.getBoolean(R.styleable.CircularImageView_civ_shadow, false)) { - shadowRadius = DEFAULT_SHADOW_RADIUS; - drawShadow(attributes.getFloat(R.styleable.CircularImageView_civ_shadow_radius, shadowRadius), - attributes.getColor(R.styleable.CircularImageView_civ_shadow_color, shadowColor)); - } - attributes.recycle(); - } - //endregion - - //region Set Attr Method - public void setBorderWidth(float borderWidth) { - this.borderWidth = borderWidth; - requestLayout(); - invalidate(); + private void loadBitmap() { + if (this.drawable == getDrawable()) { + return; } - public void setBorderColor(int borderColor) { - if (paintBorder != null) { - paintBorder.setColor(borderColor); - } - invalidate(); + this.drawable = getDrawable(); + this.image = drawableToBitmap(this.drawable); + updateShader(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + canvasSize = w; + if (h < canvasSize) { + canvasSize = h; + } + if (image != null) { + updateShader(); + } + } + + private void drawShadow(float shadowRadius, int shadowColor) { + this.shadowRadius = shadowRadius; + this.shadowColor = shadowColor; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + setLayerType(LAYER_TYPE_SOFTWARE, paintBorder); + } + paintBorder.setShadowLayer(shadowRadius, 0.0f, shadowRadius / 2, shadowColor); + } + + private void updateShader() { + if (image == null) { + return; } - public void addShadow() { - if (shadowRadius == 0) { - shadowRadius = DEFAULT_SHADOW_RADIUS; - } - drawShadow(shadowRadius, shadowColor); - invalidate(); + // Crop Center Image + image = cropBitmap(image); + + // Create Shader + BitmapShader shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + + // Center Image in Shader + Matrix matrix = new Matrix(); + matrix.setScale( + (float) canvasSize / (float) image.getWidth(), + (float) canvasSize / (float) image.getHeight()); + shader.setLocalMatrix(matrix); + + // Set Shader in Paint + paint.setShader(shader); + } + + private Bitmap cropBitmap(Bitmap bitmap) { + Bitmap bmp; + if (bitmap.getWidth() >= bitmap.getHeight()) { + bmp = + Bitmap.createBitmap( + bitmap, + bitmap.getWidth() / 2 - bitmap.getHeight() / 2, + 0, + bitmap.getHeight(), + bitmap.getHeight()); + } else { + bmp = + Bitmap.createBitmap( + bitmap, + 0, + bitmap.getHeight() / 2 - bitmap.getWidth() / 2, + bitmap.getWidth(), + bitmap.getWidth()); + } + return bmp; + } + + private Bitmap drawableToBitmap(Drawable drawable) { + if (drawable == null) { + return null; + } else if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); } - public void setShadowRadius(float shadowRadius) { - drawShadow(shadowRadius, shadowColor); - invalidate(); + int intrinsicWidth = drawable.getIntrinsicWidth(); + int intrinsicHeight = drawable.getIntrinsicHeight(); + + if (!(intrinsicWidth > 0 && intrinsicHeight > 0)) { + return null; } - public void setShadowColor(int shadowColor) { - drawShadow(shadowRadius, shadowColor); - invalidate(); + try { + // Create Bitmap object out of the drawable + Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } catch (OutOfMemoryError e) { + // Simply return null of failed bitmap creations + Log.e(getClass().toString(), "Encountered OutOfMemoryError while generating bitmap!"); + return null; + } + } + // endregion + + // region Mesure Method + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = measureWidth(widthMeasureSpec); + int height = measureHeight(heightMeasureSpec); + /*int imageSize = (width < height) ? width : height; + setMeasuredDimension(imageSize, imageSize);*/ + setMeasuredDimension(width, height); + } + + private int measureWidth(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + // The parent has determined an exact size for the child. + result = specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + // The child can be as large as it wants up to the specified size. + result = specSize; + } else { + // The parent has not imposed any constraint on the child. + result = canvasSize; } - @Override - public ScaleType getScaleType() { - return SCALE_TYPE; + return result; + } + + private int measureHeight(int measureSpecHeight) { + int result; + int specMode = MeasureSpec.getMode(measureSpecHeight); + int specSize = MeasureSpec.getSize(measureSpecHeight); + + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + result = specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + // The child can be as large as it wants up to the specified size. + result = specSize; + } else { + // Measure the text (beware: ascent is a negative number) + result = canvasSize; } - @Override - public void setScaleType(ScaleType scaleType) { - if (scaleType != SCALE_TYPE) { - throw new IllegalArgumentException(String.format( - "ScaleType %s not supported. ScaleType.CENTER_CROP is used by default. So you don't need to use ScaleType.", - scaleType)); - } - } - //endregion - - //region Draw Method - @Override - public void onDraw(Canvas canvas) { - // Load the bitmap - loadBitmap(); - - // Check if image isn't null - if (image == null) { - return; - } - - if (!isInEditMode()) { - canvasSize = canvas.getWidth(); - if (canvas.getHeight() < canvasSize) { - canvasSize = canvas.getHeight(); - } - } - - // circleCenter is the x or y of the view's center - // radius is the radius in pixels of the cirle to be drawn - // paint contains the shader that will texture the shape - int circleCenter = (int) (canvasSize - (borderWidth * 2)) / 2; - // Draw Border - canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, - circleCenter + borderWidth - (shadowRadius + shadowRadius / 2), paintBorder); - // Draw CircularImageView - canvas.drawCircle(circleCenter + borderWidth, circleCenter + borderWidth, - circleCenter - (shadowRadius + shadowRadius / 2), paint); - } - - private void loadBitmap() { - if (this.drawable == getDrawable()) { - return; - } - - this.drawable = getDrawable(); - this.image = drawableToBitmap(this.drawable); - updateShader(); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - canvasSize = w; - if (h < canvasSize) { - canvasSize = h; - } - if (image != null) { - updateShader(); - } - } - - private void drawShadow(float shadowRadius, int shadowColor) { - this.shadowRadius = shadowRadius; - this.shadowColor = shadowColor; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { - setLayerType(LAYER_TYPE_SOFTWARE, paintBorder); - } - paintBorder.setShadowLayer(shadowRadius, 0.0f, shadowRadius / 2, shadowColor); - } - - private void updateShader() { - if (image == null) { - return; - } - - // Crop Center Image - image = cropBitmap(image); - - // Create Shader - BitmapShader shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - - // Center Image in Shader - Matrix matrix = new Matrix(); - matrix.setScale((float) canvasSize / (float) image.getWidth(), - (float) canvasSize / (float) image.getHeight()); - shader.setLocalMatrix(matrix); - - // Set Shader in Paint - paint.setShader(shader); - } - - private Bitmap cropBitmap(Bitmap bitmap) { - Bitmap bmp; - if (bitmap.getWidth() >= bitmap.getHeight()) { - bmp = Bitmap.createBitmap( - bitmap, - bitmap.getWidth() / 2 - bitmap.getHeight() / 2, - 0, - bitmap.getHeight(), bitmap.getHeight()); - } else { - bmp = Bitmap.createBitmap( - bitmap, - 0, - bitmap.getHeight() / 2 - bitmap.getWidth() / 2, - bitmap.getWidth(), bitmap.getWidth()); - } - return bmp; - } - - private Bitmap drawableToBitmap(Drawable drawable) { - if (drawable == null) { - return null; - } else if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); - } - - int intrinsicWidth = drawable.getIntrinsicWidth(); - int intrinsicHeight = drawable.getIntrinsicHeight(); - - if (!(intrinsicWidth > 0 && intrinsicHeight > 0)) { - return null; - } - - try { - // Create Bitmap object out of the drawable - Bitmap bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } catch (OutOfMemoryError e) { - // Simply return null of failed bitmap creations - Log.e(getClass().toString(), "Encountered OutOfMemoryError while generating bitmap!"); - return null; - } - } - //endregion - - //region Mesure Method - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = measureWidth(widthMeasureSpec); - int height = measureHeight(heightMeasureSpec); - /*int imageSize = (width < height) ? width : height; - setMeasuredDimension(imageSize, imageSize);*/ - setMeasuredDimension(width, height); - } - - private int measureWidth(int measureSpec) { - int result; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - - if (specMode == MeasureSpec.EXACTLY) { - // The parent has determined an exact size for the child. - result = specSize; - } else if (specMode == MeasureSpec.AT_MOST) { - // The child can be as large as it wants up to the specified size. - result = specSize; - } else { - // The parent has not imposed any constraint on the child. - result = canvasSize; - } - - return result; - } - - private int measureHeight(int measureSpecHeight) { - int result; - int specMode = MeasureSpec.getMode(measureSpecHeight); - int specSize = MeasureSpec.getSize(measureSpecHeight); - - if (specMode == MeasureSpec.EXACTLY) { - // We were told how big to be - result = specSize; - } else if (specMode == MeasureSpec.AT_MOST) { - // The child can be as large as it wants up to the specified size. - result = specSize; - } else { - // Measure the text (beware: ascent is a negative number) - result = canvasSize; - } - - return (result + 2); - } - //endregion -} \ No newline at end of file + return (result + 2); + } + // endregion +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/ContributorsView.java b/app/src/main/java/code/name/monkey/retromusic/views/ContributorsView.java index f5e259046..1ce50b0ac 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/ContributorsView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/ContributorsView.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.views; +import static code.name.monkey.retromusic.util.RetroUtil.openUrl; + import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; @@ -22,55 +24,54 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import code.name.monkey.retromusic.R; -import static code.name.monkey.retromusic.util.RetroUtil.openUrl; - public class ContributorsView extends FrameLayout { - public ContributorsView(@NonNull Context context) { - super(context); - init(context, null); - } - - public ContributorsView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public ContributorsView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs); - } - - private void init(Context context, AttributeSet attributeSet) { - final TypedArray attributes = context.obtainStyledAttributes(attributeSet, R.styleable.ContributorsView, 0, 0); - if (attributes != null) { - final View layout = LayoutInflater.from(context).inflate(R.layout.item_contributor, this); - - NetworkImageView networkImageView = layout.findViewById(R.id.image); - String url = attributes.getString(R.styleable.ContributorsView_profile_url); - networkImageView.setImageUrl(url); - - String name = attributes.getString(R.styleable.ContributorsView_profile_name); - TextView title = layout.findViewById(R.id.title); - title.setText(name); - - String summary = attributes.getString(R.styleable.ContributorsView_profile_summary); - TextView text = layout.findViewById(R.id.text); - text.setText(summary); - - String link = attributes.getString(R.styleable.ContributorsView_profile_link); - layout.setOnClickListener(v -> { - if (link == null) { - return; - } - openUrl((Activity) getContext(), link); - }); - attributes.recycle(); - } + public ContributorsView(@NonNull Context context) { + super(context); + init(context, null); + } + + public ContributorsView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public ContributorsView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attributeSet) { + final TypedArray attributes = + context.obtainStyledAttributes(attributeSet, R.styleable.ContributorsView, 0, 0); + if (attributes != null) { + final View layout = LayoutInflater.from(context).inflate(R.layout.item_contributor, this); + + NetworkImageView networkImageView = layout.findViewById(R.id.image); + String url = attributes.getString(R.styleable.ContributorsView_profile_url); + networkImageView.setImageUrl(url); + + String name = attributes.getString(R.styleable.ContributorsView_profile_name); + TextView title = layout.findViewById(R.id.title); + title.setText(name); + + String summary = attributes.getString(R.styleable.ContributorsView_profile_summary); + TextView text = layout.findViewById(R.id.text); + text.setText(summary); + + String link = attributes.getString(R.styleable.ContributorsView_profile_link); + layout.setOnClickListener( + v -> { + if (link == null) { + return; + } + openUrl((Activity) getContext(), link); + }); + attributes.recycle(); } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/DrawableGradient.java b/app/src/main/java/code/name/monkey/retromusic/views/DrawableGradient.java index aad7cdbd9..e2a5daa5b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/DrawableGradient.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/DrawableGradient.java @@ -17,20 +17,19 @@ package code.name.monkey.retromusic.views; import android.graphics.drawable.GradientDrawable; public class DrawableGradient extends GradientDrawable { - public DrawableGradient(Orientation orientations, int[] colors, int shape) { - super(orientations, colors); - try { - setShape(shape); - setGradientType(GradientDrawable.LINEAR_GRADIENT); - setCornerRadius(0); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public DrawableGradient SetTransparency(int transparencyPercent) { - this.setAlpha(255 - ((255 * transparencyPercent) / 100)); - return this; + public DrawableGradient(Orientation orientations, int[] colors, int shape) { + super(orientations, colors); + try { + setShape(shape); + setGradientType(GradientDrawable.LINEAR_GRADIENT); + setCornerRadius(0); + } catch (Exception e) { + e.printStackTrace(); } + } + public DrawableGradient SetTransparency(int transparencyPercent) { + this.setAlpha(255 - ((255 * transparencyPercent) / 100)); + return this; + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/HeightFitSquareLayout.java b/app/src/main/java/code/name/monkey/retromusic/views/HeightFitSquareLayout.java index 9aebf19e7..8ed2a9e66 100755 --- a/app/src/main/java/code/name/monkey/retromusic/views/HeightFitSquareLayout.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/HeightFitSquareLayout.java @@ -20,34 +20,34 @@ import android.util.AttributeSet; import android.widget.FrameLayout; public class HeightFitSquareLayout extends FrameLayout { - private boolean forceSquare = true; + private boolean forceSquare = true; - public HeightFitSquareLayout(Context context) { - super(context); - } + public HeightFitSquareLayout(Context context) { + super(context); + } - public HeightFitSquareLayout(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - } + public HeightFitSquareLayout(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } - public HeightFitSquareLayout(Context context, AttributeSet attributeSet, int i) { - super(context, attributeSet, i); - } + public HeightFitSquareLayout(Context context, AttributeSet attributeSet, int i) { + super(context, attributeSet, i); + } - @TargetApi(21) - public HeightFitSquareLayout(Context context, AttributeSet attributeSet, int i, int i2) { - super(context, attributeSet, i, i2); - } + @TargetApi(21) + public HeightFitSquareLayout(Context context, AttributeSet attributeSet, int i, int i2) { + super(context, attributeSet, i, i2); + } - public void forceSquare(boolean z) { - this.forceSquare = z; - requestLayout(); - } + public void forceSquare(boolean z) { + this.forceSquare = z; + requestLayout(); + } - protected void onMeasure(int i, int i2) { - if (this.forceSquare) { - i = i2; - } - super.onMeasure(i, i2); + protected void onMeasure(int i, int i2) { + if (this.forceSquare) { + i = i2; } + super.onMeasure(i, i2); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/LollipopFixedWebView.java b/app/src/main/java/code/name/monkey/retromusic/views/LollipopFixedWebView.java index b599977f1..662843408 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/LollipopFixedWebView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/LollipopFixedWebView.java @@ -22,28 +22,30 @@ import android.util.AttributeSet; import android.webkit.WebView; public class LollipopFixedWebView extends WebView { - public LollipopFixedWebView(Context context) { - super(getFixedContext(context)); - } + public LollipopFixedWebView(Context context) { + super(getFixedContext(context)); + } - public LollipopFixedWebView(Context context, AttributeSet attrs) { - super(getFixedContext(context), attrs); - } + public LollipopFixedWebView(Context context, AttributeSet attrs) { + super(getFixedContext(context), attrs); + } - public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr) { - super(getFixedContext(context), attrs, defStyleAttr); - } + public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(getFixedContext(context), attrs, defStyleAttr); + } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(getFixedContext(context), attrs, defStyleAttr, defStyleRes); - } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public LollipopFixedWebView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(getFixedContext(context), attrs, defStyleAttr, defStyleRes); + } - public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { - super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing); - } + public LollipopFixedWebView( + Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { + super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing); + } - public static Context getFixedContext(Context context) { - return context.createConfigurationContext(new Configuration()); - } -} \ No newline at end of file + public static Context getFixedContext(Context context) { + return context.createConfigurationContext(new Configuration()); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/NetworkImageView.java b/app/src/main/java/code/name/monkey/retromusic/views/NetworkImageView.java index 378550bda..f115ff940 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/NetworkImageView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/NetworkImageView.java @@ -17,50 +17,47 @@ package code.name.monkey.retromusic.views; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import code.name.monkey.retromusic.R; import com.bumptech.glide.Glide; -import code.name.monkey.retromusic.R; - -/** - * @author Hemanth S (h4h13). - */ +/** @author Hemanth S (h4h13). */ public class NetworkImageView extends CircularImageView { - public NetworkImageView(@NonNull Context context) { - super(context); - init(context, null); - } + public NetworkImageView(@NonNull Context context) { + super(context); + init(context, null); + } - public NetworkImageView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } + public NetworkImageView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } - public NetworkImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs); - } + public NetworkImageView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } - public void setImageUrl(@NonNull String imageUrl) { - setImageUrl(getContext(), imageUrl); - } + public void setImageUrl(@NonNull String imageUrl) { + setImageUrl(getContext(), imageUrl); + } - public void setImageUrl(@NonNull Context context, @NonNull String imageUrl) { - Glide.with(context) - .load(imageUrl) - .error(R.drawable.ic_account) - .placeholder(R.drawable.ic_account) - .into(this); - } + public void setImageUrl(@NonNull Context context, @NonNull String imageUrl) { + Glide.with(context) + .load(imageUrl) + .error(R.drawable.ic_account) + .placeholder(R.drawable.ic_account) + .into(this); + } - private void init(Context context, AttributeSet attributeSet) { - TypedArray attributes = context.obtainStyledAttributes(attributeSet, R.styleable.NetworkImageView, 0, 0); - String url = attributes.getString(R.styleable.NetworkImageView_url_link); - setImageUrl(context, url); - attributes.recycle(); - } + private void init(Context context, AttributeSet attributeSet) { + TypedArray attributes = + context.obtainStyledAttributes(attributeSet, R.styleable.NetworkImageView, 0, 0); + String url = attributes.getString(R.styleable.NetworkImageView_url_link); + setImageUrl(context, url); + attributes.recycle(); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/PopupBackground.java b/app/src/main/java/code/name/monkey/retromusic/views/PopupBackground.java index 88b930c73..685c00892 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/PopupBackground.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/PopupBackground.java @@ -27,134 +27,135 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.view.View; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.retromusic.R; public class PopupBackground extends Drawable { - private final int mPaddingEnd; + private final int mPaddingEnd; - private final int mPaddingStart; + private final int mPaddingStart; - @NonNull - private final Paint mPaint; + @NonNull private final Paint mPaint; - @NonNull - private final Path mPath = new Path(); + @NonNull private final Path mPath = new Path(); - @NonNull - private final Matrix mTempMatrix = new Matrix(); + @NonNull private final Matrix mTempMatrix = new Matrix(); - public PopupBackground(@NonNull Context context) { - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setColor(ThemeStore.Companion.accentColor(context)); - mPaint.setStyle(Paint.Style.FILL); - Resources resources = context.getResources(); - mPaddingStart = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_start); - mPaddingEnd = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_end); + public PopupBackground(@NonNull Context context) { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(ThemeStore.Companion.accentColor(context)); + mPaint.setStyle(Paint.Style.FILL); + Resources resources = context.getResources(); + mPaddingStart = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_start); + mPaddingEnd = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_end); + } + + private static void pathArcTo( + @NonNull Path path, + float centerX, + float centerY, + float radius, + float startAngle, + float sweepAngle) { + path.arcTo( + centerX - radius, + centerY - radius, + centerX + radius, + centerY + radius, + startAngle, + sweepAngle, + false); + } + + @Override + public void draw(@NonNull Canvas canvas) { + canvas.drawPath(mPath, mPaint); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void getOutline(@NonNull Outline outline) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !mPath.isConvex()) { + // The outline path must be convex before Q, but we may run into floating point error + // caused by calculation involving sqrt(2) or OEM implementation difference, so in this + // case we just omit the shadow instead of crashing. + super.getOutline(outline); + return; } + outline.setConvexPath(mPath); + } - private static void pathArcTo(@NonNull Path path, float centerX, float centerY, float radius, - float startAngle, float sweepAngle) { - path.arcTo(centerX - radius, centerY - radius, centerX + radius, centerY + radius, - startAngle, sweepAngle, false); + @Override + public void setAlpha(int alpha) {} + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) {} + + @Override + public boolean getPadding(@NonNull Rect padding) { + if (needMirroring()) { + padding.set(mPaddingEnd, 0, mPaddingStart, 0); + } else { + padding.set(mPaddingStart, 0, mPaddingEnd, 0); } + return true; + } - @Override - public void draw(@NonNull Canvas canvas) { - canvas.drawPath(mPath, mPaint); + @Override + public boolean isAutoMirrored() { + return true; + } + + @Override + public boolean onLayoutDirectionChanged(int layoutDirection) { + updatePath(); + return true; + } + + @Override + protected void onBoundsChange(@NonNull Rect bounds) { + updatePath(); + } + + private boolean needMirroring() { + return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL; + } + + private void updatePath() { + + mPath.reset(); + + Rect bounds = getBounds(); + float width = bounds.width(); + float height = bounds.height(); + float r = height / 2; + float sqrt2 = (float) Math.sqrt(2); + // Ensure we are convex. + width = Math.max(r + sqrt2 * r, width); + pathArcTo(mPath, r, r, r, 90, 180); + float o1X = width - sqrt2 * r; + pathArcTo(mPath, o1X, r, r, -90, 45f); + float r2 = r / 5; + float o2X = width - sqrt2 * r2; + pathArcTo(mPath, o2X, r, r2, -45, 90); + pathArcTo(mPath, o1X, r, r, 45f, 45f); + mPath.close(); + + if (needMirroring()) { + mTempMatrix.setScale(-1, 1, width / 2, 0); + } else { + mTempMatrix.reset(); } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void getOutline(@NonNull Outline outline) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !mPath.isConvex()) { - // The outline path must be convex before Q, but we may run into floating point error - // caused by calculation involving sqrt(2) or OEM implementation difference, so in this - // case we just omit the shadow instead of crashing. - super.getOutline(outline); - return; - } - outline.setConvexPath(mPath); - } - - @Override - public void setAlpha(int alpha) { - - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - - } - - @Override - public boolean getPadding(@NonNull Rect padding) { - if (needMirroring()) { - padding.set(mPaddingEnd, 0, mPaddingStart, 0); - } else { - padding.set(mPaddingStart, 0, mPaddingEnd, 0); - } - return true; - } - - @Override - public boolean isAutoMirrored() { - return true; - } - - @Override - public boolean onLayoutDirectionChanged(int layoutDirection) { - updatePath(); - return true; - } - - - @Override - protected void onBoundsChange(@NonNull Rect bounds) { - updatePath(); - } - - private boolean needMirroring() { - return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL; - } - - private void updatePath() { - - mPath.reset(); - - Rect bounds = getBounds(); - float width = bounds.width(); - float height = bounds.height(); - float r = height / 2; - float sqrt2 = (float) Math.sqrt(2); - // Ensure we are convex. - width = Math.max(r + sqrt2 * r, width); - pathArcTo(mPath, r, r, r, 90, 180); - float o1X = width - sqrt2 * r; - pathArcTo(mPath, o1X, r, r, -90, 45f); - float r2 = r / 5; - float o2X = width - sqrt2 * r2; - pathArcTo(mPath, o2X, r, r2, -45, 90); - pathArcTo(mPath, o1X, r, r, 45f, 45f); - mPath.close(); - - if (needMirroring()) { - mTempMatrix.setScale(-1, 1, width / 2, 0); - } else { - mTempMatrix.reset(); - } - mTempMatrix.postTranslate(bounds.left, bounds.top); - mPath.transform(mTempMatrix); - } -} \ No newline at end of file + mTempMatrix.postTranslate(bounds.left, bounds.top); + mPath.transform(mTempMatrix); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/ScrollingViewOnApplyWindowInsetsListener.java b/app/src/main/java/code/name/monkey/retromusic/views/ScrollingViewOnApplyWindowInsetsListener.java index 1abe0bac4..7f2d049a0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/ScrollingViewOnApplyWindowInsetsListener.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/ScrollingViewOnApplyWindowInsetsListener.java @@ -17,42 +17,46 @@ package code.name.monkey.retromusic.views; import android.graphics.Rect; import android.view.View; import android.view.WindowInsets; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import me.zhanghai.android.fastscroll.FastScroller; public class ScrollingViewOnApplyWindowInsetsListener implements View.OnApplyWindowInsetsListener { - @NonNull - private final Rect mPadding = new Rect(); - @Nullable - private final FastScroller mFastScroller; + @NonNull private final Rect mPadding = new Rect(); + @Nullable private final FastScroller mFastScroller; - public ScrollingViewOnApplyWindowInsetsListener(@Nullable View view, - @Nullable FastScroller fastScroller) { - if (view != null) { - mPadding.set(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), - view.getPaddingBottom()); - } - mFastScroller = fastScroller; + public ScrollingViewOnApplyWindowInsetsListener( + @Nullable View view, @Nullable FastScroller fastScroller) { + if (view != null) { + mPadding.set( + view.getPaddingLeft(), + view.getPaddingTop(), + view.getPaddingRight(), + view.getPaddingBottom()); } + mFastScroller = fastScroller; + } - public ScrollingViewOnApplyWindowInsetsListener() { - this(null, null); - } + public ScrollingViewOnApplyWindowInsetsListener() { + this(null, null); + } - @NonNull - @Override - public WindowInsets onApplyWindowInsets(@NonNull View view, @NonNull WindowInsets insets) { - view.setPadding(mPadding.left + insets.getSystemWindowInsetLeft(), mPadding.top, - mPadding.right + insets.getSystemWindowInsetRight(), - mPadding.bottom + insets.getSystemWindowInsetBottom()); - if (mFastScroller != null) { - mFastScroller.setPadding(insets.getSystemWindowInsetLeft(), 0, - insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); - } - return insets; + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View view, @NonNull WindowInsets insets) { + view.setPadding( + mPadding.left + insets.getSystemWindowInsetLeft(), + mPadding.top, + mPadding.right + insets.getSystemWindowInsetRight(), + mPadding.bottom + insets.getSystemWindowInsetBottom()); + if (mFastScroller != null) { + mFastScroller.setPadding( + insets.getSystemWindowInsetLeft(), + 0, + insets.getSystemWindowInsetRight(), + insets.getSystemWindowInsetBottom()); } -} \ No newline at end of file + return insets; + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/SeekArc.java b/app/src/main/java/code/name/monkey/retromusic/views/SeekArc.java index 684410fc3..82b6b546d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/SeekArc.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/SeekArc.java @@ -24,529 +24,488 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; - import code.name.monkey.retromusic.R; /** * SeekArc.java - *

- * This is a class that functions much like a SeekBar but - * follows a circle path instead of a straight line. + * + *

This is a class that functions much like a SeekBar but follows a circle path instead of a + * straight line. * * @author Neil Davies */ public class SeekArc extends View { - private static final String TAG = SeekArc.class.getSimpleName(); - private static int INVALID_PROGRESS_VALUE = -1; - // The initial rotational offset -90 means we start at 12 o'clock - private final int mAngleOffset = -90; - private Paint mArcPaint; - // Internal variables - private int mArcRadius = 0; - private RectF mArcRect = new RectF(); - /** - * The Width of the background arc for the SeekArc - */ - private int mArcWidth = 2; - /** - * Will the progress increase clockwise or anti-clockwise - */ - private boolean mClockwise = true; - /** - * is the control enabled/touchable - */ - private boolean mEnabled = true; - /** - * The Maximum value that this SeekArc can be set to - */ - private int mMax = 100; - private OnSeekArcChangeListener mOnSeekArcChangeListener; - /** - * The Current value that the SeekArc is set to - */ - private int mProgress = 0; - private Paint mProgressPaint; - private float mProgressSweep = 0; - /** - * The width of the progress line for this SeekArc - */ - private int mProgressWidth = 4; - /** - * The rotation of the SeekArc- 0 is twelve o'clock - */ - private int mRotation = 0; - /** - * Give the SeekArc rounded edges - */ - private boolean mRoundedEdges = false; - /** - * The Angle to start drawing this Arc from - */ - private int mStartAngle = 0; - /** - * The Angle through which to draw the arc (Max is 360) - */ - private int mSweepAngle = 360; - /** - * The Drawable for the seek arc thumbnail - */ - private Drawable mThumb; - private int mThumbXPos; - private int mThumbYPos; - private double mTouchAngle; - private float mTouchIgnoreRadius; - /** - * Enable touch inside the SeekArc - */ - private boolean mTouchInside = true; - private int mTranslateX; - private int mTranslateY; + private static final String TAG = SeekArc.class.getSimpleName(); + private static int INVALID_PROGRESS_VALUE = -1; + // The initial rotational offset -90 means we start at 12 o'clock + private final int mAngleOffset = -90; + private Paint mArcPaint; + // Internal variables + private int mArcRadius = 0; + private RectF mArcRect = new RectF(); + /** The Width of the background arc for the SeekArc */ + private int mArcWidth = 2; + /** Will the progress increase clockwise or anti-clockwise */ + private boolean mClockwise = true; + /** is the control enabled/touchable */ + private boolean mEnabled = true; + /** The Maximum value that this SeekArc can be set to */ + private int mMax = 100; - public SeekArc(Context context) { - super(context); - init(context, null, 0); + private OnSeekArcChangeListener mOnSeekArcChangeListener; + /** The Current value that the SeekArc is set to */ + private int mProgress = 0; + + private Paint mProgressPaint; + private float mProgressSweep = 0; + /** The width of the progress line for this SeekArc */ + private int mProgressWidth = 4; + /** The rotation of the SeekArc- 0 is twelve o'clock */ + private int mRotation = 0; + /** Give the SeekArc rounded edges */ + private boolean mRoundedEdges = false; + /** The Angle to start drawing this Arc from */ + private int mStartAngle = 0; + /** The Angle through which to draw the arc (Max is 360) */ + private int mSweepAngle = 360; + /** The Drawable for the seek arc thumbnail */ + private Drawable mThumb; + + private int mThumbXPos; + private int mThumbYPos; + private double mTouchAngle; + private float mTouchIgnoreRadius; + /** Enable touch inside the SeekArc */ + private boolean mTouchInside = true; + + private int mTranslateX; + private int mTranslateY; + + public SeekArc(Context context) { + super(context); + init(context, null, 0); + } + + public SeekArc(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, R.attr.seekArcStyle); + } + + public SeekArc(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + public int getArcColor() { + return mArcPaint.getColor(); + } + + public void setArcColor(int color) { + mArcPaint.setColor(color); + invalidate(); + } + + public int getArcRotation() { + return mRotation; + } + + public void setArcRotation(int mRotation) { + this.mRotation = mRotation; + updateThumbPosition(); + } + + public int getArcWidth() { + return mArcWidth; + } + + public void setArcWidth(int mArcWidth) { + this.mArcWidth = mArcWidth; + mArcPaint.setStrokeWidth(mArcWidth); + } + + public int getMax() { + return mMax; + } + + public void setMax(int mMax) { + this.mMax = mMax; + } + + public int getProgress() { + return mProgress; + } + + public void setProgress(int progress) { + updateProgress(progress, false); + } + + public int getProgressColor() { + return mProgressPaint.getColor(); + } + + public void setProgressColor(int color) { + mProgressPaint.setColor(color); + invalidate(); + } + + public int getProgressWidth() { + return mProgressWidth; + } + + public void setProgressWidth(int mProgressWidth) { + this.mProgressWidth = mProgressWidth; + mProgressPaint.setStrokeWidth(mProgressWidth); + } + + public int getStartAngle() { + return mStartAngle; + } + + public void setStartAngle(int mStartAngle) { + this.mStartAngle = mStartAngle; + updateThumbPosition(); + } + + public int getSweepAngle() { + return mSweepAngle; + } + + public void setSweepAngle(int mSweepAngle) { + this.mSweepAngle = mSweepAngle; + updateThumbPosition(); + } + + public boolean isClockwise() { + return mClockwise; + } + + public void setClockwise(boolean isClockwise) { + mClockwise = isClockwise; + } + + public boolean isEnabled() { + return mEnabled; + } + + public void setEnabled(boolean enabled) { + this.mEnabled = enabled; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mEnabled) { + this.getParent().requestDisallowInterceptTouchEvent(true); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartTrackingTouch(); + updateOnTouch(event); + break; + case MotionEvent.ACTION_MOVE: + updateOnTouch(event); + break; + case MotionEvent.ACTION_UP: + onStopTrackingTouch(); + setPressed(false); + this.getParent().requestDisallowInterceptTouchEvent(false); + break; + case MotionEvent.ACTION_CANCEL: + onStopTrackingTouch(); + setPressed(false); + this.getParent().requestDisallowInterceptTouchEvent(false); + break; + } + return true; + } + return false; + } + + /** + * Sets a listener to receive notifications of changes to the SeekArc's progress level. Also + * provides notifications of when the user starts and stops a touch gesture within the SeekArc. + * + * @param l The seek bar notification listener + * @see SeekArc.OnSeekBarChangeListener + */ + public void setOnSeekArcChangeListener(OnSeekArcChangeListener l) { + mOnSeekArcChangeListener = l; + } + + public void setRoundedEdges(boolean isEnabled) { + mRoundedEdges = isEnabled; + if (mRoundedEdges) { + mArcPaint.setStrokeCap(Paint.Cap.ROUND); + mProgressPaint.setStrokeCap(Paint.Cap.ROUND); + } else { + mArcPaint.setStrokeCap(Paint.Cap.SQUARE); + mProgressPaint.setStrokeCap(Paint.Cap.SQUARE); + } + } + + public void setTouchInSide(boolean isEnabled) { + int thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; + int thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; + mTouchInside = isEnabled; + if (mTouchInside) { + mTouchIgnoreRadius = (float) mArcRadius / 4; + } else { + // Don't use the exact radius makes interaction too tricky + mTouchIgnoreRadius = mArcRadius - Math.min(thumbHalfWidth, thumbHalfheight); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mThumb != null && mThumb.isStateful()) { + int[] state = getDrawableState(); + mThumb.setState(state); + } + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + if (!mClockwise) { + canvas.scale(-1, 1, mArcRect.centerX(), mArcRect.centerY()); } - public SeekArc(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, R.attr.seekArcStyle); + // Draw the arcs + final int arcStart = mStartAngle + mAngleOffset + mRotation; + final int arcSweep = mSweepAngle; + canvas.drawArc(mArcRect, arcStart, arcSweep, false, mArcPaint); + canvas.drawArc(mArcRect, arcStart, mProgressSweep, false, mProgressPaint); + + if (mEnabled) { + // Draw the thumb nail + canvas.translate(mTranslateX - mThumbXPos, mTranslateY - mThumbYPos); + mThumb.draw(canvas); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + final int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); + final int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); + final int min = Math.min(width, height); + float top = 0; + float left = 0; + int arcDiameter = 0; + + mTranslateX = (int) (width * 0.5f); + mTranslateY = (int) (height * 0.5f); + + arcDiameter = min - getPaddingLeft(); + mArcRadius = arcDiameter / 2; + top = height / 2 - (arcDiameter / 2); + left = width / 2 - (arcDiameter / 2); + mArcRect.set(left, top, left + arcDiameter, top + arcDiameter); + + int arcStart = (int) mProgressSweep + mStartAngle + mRotation + 90; + mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(arcStart))); + mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(arcStart))); + + setTouchInSide(mTouchInside); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + private int getProgressForAngle(double angle) { + int touchProgress = (int) Math.round(valuePerDegree() * angle); + + touchProgress = (touchProgress < 0) ? INVALID_PROGRESS_VALUE : touchProgress; + touchProgress = (touchProgress > mMax) ? INVALID_PROGRESS_VALUE : touchProgress; + return touchProgress; + } + + private double getTouchDegrees(float xPos, float yPos) { + float x = xPos - mTranslateX; + float y = yPos - mTranslateY; + // invert the x-coord if we are rotating anti-clockwise + x = (mClockwise) ? x : -x; + // convert to arc Angle + double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) - Math.toRadians(mRotation)); + if (angle < 0) { + angle = 360 + angle; + } + angle -= mStartAngle; + return angle; + } + + private boolean ignoreTouch(float xPos, float yPos) { + boolean ignore = false; + float x = xPos - mTranslateX; + float y = yPos - mTranslateY; + + float touchRadius = (float) Math.sqrt(((x * x) + (y * y))); + if (touchRadius < mTouchIgnoreRadius) { + ignore = true; + } + return ignore; + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + + Log.d(TAG, "Initialising SeekArc"); + final Resources res = getResources(); + float density = context.getResources().getDisplayMetrics().density; + + // Defaults, may need to link this into theme settings + int arcColor = res.getColor(R.color.progress_gray); + int progressColor = res.getColor(R.color.default_blue_light); + int thumbHalfheight = 0; + int thumbHalfWidth = 0; + mThumb = res.getDrawable(R.drawable.switch_thumb_material); + // Convert progress width to pixels for current density + mProgressWidth = (int) (mProgressWidth * density); + + if (attrs != null) { + // Attribute initialization + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SeekArc, defStyle, 0); + + Drawable thumb = a.getDrawable(R.styleable.SeekArc_thumb); + if (thumb != null) { + mThumb = thumb; + } + + thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; + thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; + mThumb.setBounds(-thumbHalfWidth, -thumbHalfheight, thumbHalfWidth, thumbHalfheight); + + mMax = a.getInteger(R.styleable.SeekArc_max, mMax); + mProgress = a.getInteger(R.styleable.SeekArc_seekProgress, mProgress); + mProgressWidth = (int) a.getDimension(R.styleable.SeekArc_progressWidth, mProgressWidth); + mArcWidth = (int) a.getDimension(R.styleable.SeekArc_arcWidth, mArcWidth); + mStartAngle = a.getInt(R.styleable.SeekArc_startAngle, mStartAngle); + mSweepAngle = a.getInt(R.styleable.SeekArc_sweepAngle, mSweepAngle); + mRotation = a.getInt(R.styleable.SeekArc_rotation, mRotation); + mRoundedEdges = a.getBoolean(R.styleable.SeekArc_roundEdges, mRoundedEdges); + mTouchInside = a.getBoolean(R.styleable.SeekArc_touchInside, mTouchInside); + mClockwise = a.getBoolean(R.styleable.SeekArc_clockwise, mClockwise); + mEnabled = a.getBoolean(R.styleable.SeekArc_enabled, mEnabled); + + arcColor = a.getColor(R.styleable.SeekArc_arcColor, arcColor); + progressColor = a.getColor(R.styleable.SeekArc_progressColor, progressColor); + + a.recycle(); } - public SeekArc(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context, attrs, defStyle); + mProgress = (mProgress > mMax) ? mMax : mProgress; + mProgress = (mProgress < 0) ? 0 : mProgress; + + mSweepAngle = (mSweepAngle > 360) ? 360 : mSweepAngle; + mSweepAngle = (mSweepAngle < 0) ? 0 : mSweepAngle; + + mProgressSweep = (float) mProgress / mMax * mSweepAngle; + + mStartAngle = (mStartAngle > 360) ? 0 : mStartAngle; + mStartAngle = (mStartAngle < 0) ? 0 : mStartAngle; + + mArcPaint = new Paint(); + mArcPaint.setColor(arcColor); + mArcPaint.setAntiAlias(true); + mArcPaint.setStyle(Paint.Style.STROKE); + mArcPaint.setStrokeWidth(mArcWidth); + // mArcPaint.setAlpha(45); + + mProgressPaint = new Paint(); + mProgressPaint.setColor(progressColor); + mProgressPaint.setAntiAlias(true); + mProgressPaint.setStyle(Paint.Style.STROKE); + mProgressPaint.setStrokeWidth(mProgressWidth); + + if (mRoundedEdges) { + mArcPaint.setStrokeCap(Paint.Cap.ROUND); + mProgressPaint.setStrokeCap(Paint.Cap.ROUND); + } + } + + private void onProgressRefresh(int progress, boolean fromUser) { + updateProgress(progress, fromUser); + } + + private void onStartTrackingTouch() { + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener.onStartTrackingTouch(this); + } + } + + private void onStopTrackingTouch() { + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener.onStopTrackingTouch(this); + } + } + + private void updateOnTouch(MotionEvent event) { + boolean ignoreTouch = ignoreTouch(event.getX(), event.getY()); + if (ignoreTouch) { + return; + } + setPressed(true); + mTouchAngle = getTouchDegrees(event.getX(), event.getY()); + int progress = getProgressForAngle(mTouchAngle); + onProgressRefresh(progress, true); + } + + private void updateProgress(int progress, boolean fromUser) { + + if (progress == INVALID_PROGRESS_VALUE) { + return; } - public int getArcColor() { - return mArcPaint.getColor(); + progress = (progress > mMax) ? mMax : progress; + progress = (progress < 0) ? 0 : progress; + mProgress = progress; + + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener.onProgressChanged(this, progress, fromUser); } - public void setArcColor(int color) { - mArcPaint.setColor(color); - invalidate(); - } + mProgressSweep = (float) progress / mMax * mSweepAngle; - public int getArcRotation() { - return mRotation; - } + updateThumbPosition(); - public void setArcRotation(int mRotation) { - this.mRotation = mRotation; - updateThumbPosition(); - } + invalidate(); + } - public int getArcWidth() { - return mArcWidth; - } + private void updateThumbPosition() { + int thumbAngle = (int) (mStartAngle + mProgressSweep + mRotation + 90); + mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(thumbAngle))); + mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(thumbAngle))); + } - public void setArcWidth(int mArcWidth) { - this.mArcWidth = mArcWidth; - mArcPaint.setStrokeWidth(mArcWidth); - } + private float valuePerDegree() { + return (float) mMax / mSweepAngle; + } - public int getMax() { - return mMax; - } - - public void setMax(int mMax) { - this.mMax = mMax; - } - - public int getProgress() { - return mProgress; - } - - public void setProgress(int progress) { - updateProgress(progress, false); - } - - public int getProgressColor() { - return mProgressPaint.getColor(); - } - - public void setProgressColor(int color) { - mProgressPaint.setColor(color); - invalidate(); - } - - public int getProgressWidth() { - return mProgressWidth; - } - - public void setProgressWidth(int mProgressWidth) { - this.mProgressWidth = mProgressWidth; - mProgressPaint.setStrokeWidth(mProgressWidth); - } - - public int getStartAngle() { - return mStartAngle; - } - - public void setStartAngle(int mStartAngle) { - this.mStartAngle = mStartAngle; - updateThumbPosition(); - } - - public int getSweepAngle() { - return mSweepAngle; - } - - public void setSweepAngle(int mSweepAngle) { - this.mSweepAngle = mSweepAngle; - updateThumbPosition(); - } - - public boolean isClockwise() { - return mClockwise; - } - - public void setClockwise(boolean isClockwise) { - mClockwise = isClockwise; - } - - public boolean isEnabled() { - return mEnabled; - } - - public void setEnabled(boolean enabled) { - this.mEnabled = enabled; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mEnabled) { - this.getParent().requestDisallowInterceptTouchEvent(true); - - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onStartTrackingTouch(); - updateOnTouch(event); - break; - case MotionEvent.ACTION_MOVE: - updateOnTouch(event); - break; - case MotionEvent.ACTION_UP: - onStopTrackingTouch(); - setPressed(false); - this.getParent().requestDisallowInterceptTouchEvent(false); - break; - case MotionEvent.ACTION_CANCEL: - onStopTrackingTouch(); - setPressed(false); - this.getParent().requestDisallowInterceptTouchEvent(false); - break; - } - return true; - } - return false; - } + public interface OnSeekArcChangeListener { /** - * Sets a listener to receive notifications of changes to the SeekArc's - * progress level. Also provides notifications of when the user starts and - * stops a touch gesture within the SeekArc. + * Notification that the progress level has changed. Clients can use the fromUser parameter to + * distinguish user-initiated changes from those that occurred programmatically. * - * @param l The seek bar notification listener - * @see SeekArc.OnSeekBarChangeListener + * @param seekArc The SeekArc whose progress has changed + * @param progress The current progress level. This will be in the range 0..max where max was + * set by {@link ProgressArc#setMax(int)}. (The default value for max is 100.) + * @param fromUser True if the progress change was initiated by the user. */ - public void setOnSeekArcChangeListener(OnSeekArcChangeListener l) { - mOnSeekArcChangeListener = l; - } + void onProgressChanged(SeekArc seekArc, int progress, boolean fromUser); - public void setRoundedEdges(boolean isEnabled) { - mRoundedEdges = isEnabled; - if (mRoundedEdges) { - mArcPaint.setStrokeCap(Paint.Cap.ROUND); - mProgressPaint.setStrokeCap(Paint.Cap.ROUND); - } else { - mArcPaint.setStrokeCap(Paint.Cap.SQUARE); - mProgressPaint.setStrokeCap(Paint.Cap.SQUARE); - } - } + /** + * Notification that the user has started a touch gesture. Clients may want to use this to + * disable advancing the seekbar. + * + * @param seekArc The SeekArc in which the touch gesture began + */ + void onStartTrackingTouch(SeekArc seekArc); - public void setTouchInSide(boolean isEnabled) { - int thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; - int thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; - mTouchInside = isEnabled; - if (mTouchInside) { - mTouchIgnoreRadius = (float) mArcRadius / 4; - } else { - // Don't use the exact radius makes interaction too tricky - mTouchIgnoreRadius = mArcRadius - - Math.min(thumbHalfWidth, thumbHalfheight); - } - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - if (mThumb != null && mThumb.isStateful()) { - int[] state = getDrawableState(); - mThumb.setState(state); - } - invalidate(); - } - - @Override - protected void onDraw(Canvas canvas) { - if (!mClockwise) { - canvas.scale(-1, 1, mArcRect.centerX(), mArcRect.centerY()); - } - - // Draw the arcs - final int arcStart = mStartAngle + mAngleOffset + mRotation; - final int arcSweep = mSweepAngle; - canvas.drawArc(mArcRect, arcStart, arcSweep, false, mArcPaint); - canvas.drawArc(mArcRect, arcStart, mProgressSweep, false, - mProgressPaint); - - if (mEnabled) { - // Draw the thumb nail - canvas.translate(mTranslateX - mThumbXPos, mTranslateY - mThumbYPos); - mThumb.draw(canvas); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - - final int height = getDefaultSize(getSuggestedMinimumHeight(), - heightMeasureSpec); - final int width = getDefaultSize(getSuggestedMinimumWidth(), - widthMeasureSpec); - final int min = Math.min(width, height); - float top = 0; - float left = 0; - int arcDiameter = 0; - - mTranslateX = (int) (width * 0.5f); - mTranslateY = (int) (height * 0.5f); - - arcDiameter = min - getPaddingLeft(); - mArcRadius = arcDiameter / 2; - top = height / 2 - (arcDiameter / 2); - left = width / 2 - (arcDiameter / 2); - mArcRect.set(left, top, left + arcDiameter, top + arcDiameter); - - int arcStart = (int) mProgressSweep + mStartAngle + mRotation + 90; - mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(arcStart))); - mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(arcStart))); - - setTouchInSide(mTouchInside); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - private int getProgressForAngle(double angle) { - int touchProgress = (int) Math.round(valuePerDegree() * angle); - - touchProgress = (touchProgress < 0) ? INVALID_PROGRESS_VALUE - : touchProgress; - touchProgress = (touchProgress > mMax) ? INVALID_PROGRESS_VALUE - : touchProgress; - return touchProgress; - } - - private double getTouchDegrees(float xPos, float yPos) { - float x = xPos - mTranslateX; - float y = yPos - mTranslateY; - //invert the x-coord if we are rotating anti-clockwise - x = (mClockwise) ? x : -x; - // convert to arc Angle - double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) - - Math.toRadians(mRotation)); - if (angle < 0) { - angle = 360 + angle; - } - angle -= mStartAngle; - return angle; - } - - private boolean ignoreTouch(float xPos, float yPos) { - boolean ignore = false; - float x = xPos - mTranslateX; - float y = yPos - mTranslateY; - - float touchRadius = (float) Math.sqrt(((x * x) + (y * y))); - if (touchRadius < mTouchIgnoreRadius) { - ignore = true; - } - return ignore; - } - - private void init(Context context, AttributeSet attrs, int defStyle) { - - Log.d(TAG, "Initialising SeekArc"); - final Resources res = getResources(); - float density = context.getResources().getDisplayMetrics().density; - - // Defaults, may need to link this into theme settings - int arcColor = res.getColor(R.color.progress_gray); - int progressColor = res.getColor(R.color.default_blue_light); - int thumbHalfheight = 0; - int thumbHalfWidth = 0; - mThumb = res.getDrawable(R.drawable.switch_thumb_material); - // Convert progress width to pixels for current density - mProgressWidth = (int) (mProgressWidth * density); - - if (attrs != null) { - // Attribute initialization - final TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.SeekArc, defStyle, 0); - - Drawable thumb = a.getDrawable(R.styleable.SeekArc_thumb); - if (thumb != null) { - mThumb = thumb; - } - - thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; - thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; - mThumb.setBounds(-thumbHalfWidth, -thumbHalfheight, thumbHalfWidth, - thumbHalfheight); - - mMax = a.getInteger(R.styleable.SeekArc_max, mMax); - mProgress = a.getInteger(R.styleable.SeekArc_seekProgress, mProgress); - mProgressWidth = (int) a.getDimension( - R.styleable.SeekArc_progressWidth, mProgressWidth); - mArcWidth = (int) a.getDimension(R.styleable.SeekArc_arcWidth, - mArcWidth); - mStartAngle = a.getInt(R.styleable.SeekArc_startAngle, mStartAngle); - mSweepAngle = a.getInt(R.styleable.SeekArc_sweepAngle, mSweepAngle); - mRotation = a.getInt(R.styleable.SeekArc_rotation, mRotation); - mRoundedEdges = a.getBoolean(R.styleable.SeekArc_roundEdges, - mRoundedEdges); - mTouchInside = a.getBoolean(R.styleable.SeekArc_touchInside, - mTouchInside); - mClockwise = a.getBoolean(R.styleable.SeekArc_clockwise, - mClockwise); - mEnabled = a.getBoolean(R.styleable.SeekArc_enabled, mEnabled); - - arcColor = a.getColor(R.styleable.SeekArc_arcColor, arcColor); - progressColor = a.getColor(R.styleable.SeekArc_progressColor, - progressColor); - - a.recycle(); - } - - mProgress = (mProgress > mMax) ? mMax : mProgress; - mProgress = (mProgress < 0) ? 0 : mProgress; - - mSweepAngle = (mSweepAngle > 360) ? 360 : mSweepAngle; - mSweepAngle = (mSweepAngle < 0) ? 0 : mSweepAngle; - - mProgressSweep = (float) mProgress / mMax * mSweepAngle; - - mStartAngle = (mStartAngle > 360) ? 0 : mStartAngle; - mStartAngle = (mStartAngle < 0) ? 0 : mStartAngle; - - mArcPaint = new Paint(); - mArcPaint.setColor(arcColor); - mArcPaint.setAntiAlias(true); - mArcPaint.setStyle(Paint.Style.STROKE); - mArcPaint.setStrokeWidth(mArcWidth); - //mArcPaint.setAlpha(45); - - mProgressPaint = new Paint(); - mProgressPaint.setColor(progressColor); - mProgressPaint.setAntiAlias(true); - mProgressPaint.setStyle(Paint.Style.STROKE); - mProgressPaint.setStrokeWidth(mProgressWidth); - - if (mRoundedEdges) { - mArcPaint.setStrokeCap(Paint.Cap.ROUND); - mProgressPaint.setStrokeCap(Paint.Cap.ROUND); - } - } - - private void onProgressRefresh(int progress, boolean fromUser) { - updateProgress(progress, fromUser); - } - - private void onStartTrackingTouch() { - if (mOnSeekArcChangeListener != null) { - mOnSeekArcChangeListener.onStartTrackingTouch(this); - } - } - - private void onStopTrackingTouch() { - if (mOnSeekArcChangeListener != null) { - mOnSeekArcChangeListener.onStopTrackingTouch(this); - } - } - - private void updateOnTouch(MotionEvent event) { - boolean ignoreTouch = ignoreTouch(event.getX(), event.getY()); - if (ignoreTouch) { - return; - } - setPressed(true); - mTouchAngle = getTouchDegrees(event.getX(), event.getY()); - int progress = getProgressForAngle(mTouchAngle); - onProgressRefresh(progress, true); - } - - private void updateProgress(int progress, boolean fromUser) { - - if (progress == INVALID_PROGRESS_VALUE) { - return; - } - - progress = (progress > mMax) ? mMax : progress; - progress = (progress < 0) ? 0 : progress; - mProgress = progress; - - if (mOnSeekArcChangeListener != null) { - mOnSeekArcChangeListener - .onProgressChanged(this, progress, fromUser); - } - - mProgressSweep = (float) progress / mMax * mSweepAngle; - - updateThumbPosition(); - - invalidate(); - } - - private void updateThumbPosition() { - int thumbAngle = (int) (mStartAngle + mProgressSweep + mRotation + 90); - mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(thumbAngle))); - mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(thumbAngle))); - } - - private float valuePerDegree() { - return (float) mMax / mSweepAngle; - } - - public interface OnSeekArcChangeListener { - - /** - * Notification that the progress level has changed. Clients can use the - * fromUser parameter to distinguish user-initiated changes from those - * that occurred programmatically. - * - * @param seekArc The SeekArc whose progress has changed - * @param progress The current progress level. This will be in the range - * 0..max where max was set by - * {@link ProgressArc#setMax(int)}. (The default value for - * max is 100.) - * @param fromUser True if the progress change was initiated by the user. - */ - void onProgressChanged(SeekArc seekArc, int progress, boolean fromUser); - - /** - * Notification that the user has started a touch gesture. Clients may - * want to use this to disable advancing the seekbar. - * - * @param seekArc The SeekArc in which the touch gesture began - */ - void onStartTrackingTouch(SeekArc seekArc); - - /** - * Notification that the user has finished a touch gesture. Clients may - * want to use this to re-enable advancing the seekarc. - * - * @param seekArc The SeekArc in which the touch gesture began - */ - void onStopTrackingTouch(SeekArc seekArc); - } -} \ No newline at end of file + /** + * Notification that the user has finished a touch gesture. Clients may want to use this to + * re-enable advancing the seekarc. + * + * @param seekArc The SeekArc in which the touch gesture began + */ + void onStopTrackingTouch(SeekArc seekArc); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/StatusBarMarginFrameLayout.java b/app/src/main/java/code/name/monkey/retromusic/views/StatusBarMarginFrameLayout.java index e9dfc3866..f4bc8f8fb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/StatusBarMarginFrameLayout.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/StatusBarMarginFrameLayout.java @@ -19,33 +19,32 @@ import android.os.Build; import android.util.AttributeSet; import android.view.WindowInsets; import android.widget.FrameLayout; - import androidx.annotation.NonNull; public class StatusBarMarginFrameLayout extends FrameLayout { + public StatusBarMarginFrameLayout(@NonNull Context context) { + super(context); + } - public StatusBarMarginFrameLayout(@NonNull Context context) { - super(context); - } + public StatusBarMarginFrameLayout(@NonNull Context context, @NonNull AttributeSet attrs) { + super(context, attrs); + } - public StatusBarMarginFrameLayout(@NonNull Context context, @NonNull AttributeSet attrs) { - super(context, attrs); - } + public StatusBarMarginFrameLayout( + @NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } - public StatusBarMarginFrameLayout(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @NonNull - @Override - public WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); - lp.topMargin = insets.getSystemWindowInsetTop(); - lp.bottomMargin = insets.getSystemWindowInsetBottom(); - setLayoutParams(lp); - } - return super.onApplyWindowInsets(insets); + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); + lp.topMargin = insets.getSystemWindowInsetTop(); + lp.bottomMargin = insets.getSystemWindowInsetBottom(); + setLayoutParams(lp); } + return super.onApplyWindowInsets(insets); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/StatusBarView.java b/app/src/main/java/code/name/monkey/retromusic/views/StatusBarView.java index 8eaf325d4..c149cc3da 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/StatusBarView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/StatusBarView.java @@ -18,42 +18,38 @@ import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.View; - import androidx.annotation.NonNull; public class StatusBarView extends View { + public StatusBarView(@NonNull Context context) { + super(context); + init(context); + } - public StatusBarView(@NonNull Context context) { - super(context); - init(context); + public StatusBarView(@NonNull Context context, @NonNull AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public StatusBarView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public static int getStatusBarHeight(@NonNull Resources r) { + int result = 0; + int resourceId = r.getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = r.getDimensionPixelSize(resourceId); } + return result; + } - public StatusBarView(@NonNull Context context, @NonNull AttributeSet attrs) { - super(context, attrs); - init(context); - } + private void init(Context context) {} - public StatusBarView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context); - } - - public static int getStatusBarHeight(@NonNull Resources r) { - int result = 0; - int resourceId = r.getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - result = r.getDimensionPixelSize(resourceId); - } - return result; - } - - private void init(Context context) { - - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), getStatusBarHeight(getResources())); - } -} \ No newline at end of file + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), getStatusBarHeight(getResources())); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/views/VerticalTextView.java b/app/src/main/java/code/name/monkey/retromusic/views/VerticalTextView.java index 5e79c0967..1172bb6af 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/VerticalTextView.java +++ b/app/src/main/java/code/name/monkey/retromusic/views/VerticalTextView.java @@ -19,50 +19,46 @@ import android.graphics.Canvas; import android.text.TextPaint; import android.util.AttributeSet; import android.view.Gravity; - import androidx.appcompat.widget.AppCompatTextView; - public class VerticalTextView extends AppCompatTextView { - final boolean topDown; + final boolean topDown; - public VerticalTextView(Context context, AttributeSet attrs) { - super(context, attrs); - final int gravity = getGravity(); - if (Gravity.isVertical(gravity) && (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { - setGravity((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP); - topDown = false; - } else - topDown = true; + public VerticalTextView(Context context, AttributeSet attrs) { + super(context, attrs); + final int gravity = getGravity(); + if (Gravity.isVertical(gravity) + && (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { + setGravity((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP); + topDown = false; + } else topDown = true; + } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(heightMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); + } + + @Override + protected void onDraw(Canvas canvas) { + TextPaint textPaint = getPaint(); + textPaint.setColor(getCurrentTextColor()); + textPaint.drawableState = getDrawableState(); + + canvas.save(); + + if (topDown) { + canvas.translate(getWidth(), 0); + canvas.rotate(90); + } else { + canvas.translate(0, getHeight()); + canvas.rotate(-90); } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(heightMeasureSpec, widthMeasureSpec); - setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth()); - } + canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); - @Override - protected void onDraw(Canvas canvas) { - TextPaint textPaint = getPaint(); - textPaint.setColor(getCurrentTextColor()); - textPaint.drawableState = getDrawableState(); - - canvas.save(); - - if (topDown) { - canvas.translate(getWidth(), 0); - canvas.rotate(90); - } else { - canvas.translate(0, getHeight()); - canvas.rotate(-90); - } - - - canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); - - getLayout().draw(canvas); - canvas.restore(); - } -} \ No newline at end of file + getLayout().draw(canvas); + canvas.restore(); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/volume/AudioVolumeContentObserver.java b/app/src/main/java/code/name/monkey/retromusic/volume/AudioVolumeContentObserver.java index 7e1ec7c3b..cea4a85e4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/volume/AudioVolumeContentObserver.java +++ b/app/src/main/java/code/name/monkey/retromusic/volume/AudioVolumeContentObserver.java @@ -18,47 +18,46 @@ import android.database.ContentObserver; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; - import androidx.annotation.NonNull; public class AudioVolumeContentObserver extends ContentObserver { - private final OnAudioVolumeChangedListener mListener; + private final OnAudioVolumeChangedListener mListener; - private final AudioManager mAudioManager; + private final AudioManager mAudioManager; - private final int mAudioStreamType; + private final int mAudioStreamType; - private float mLastVolume; + private float mLastVolume; - AudioVolumeContentObserver(@NonNull Handler handler, @NonNull AudioManager audioManager, - int audioStreamType, - @NonNull OnAudioVolumeChangedListener listener) { + AudioVolumeContentObserver( + @NonNull Handler handler, + @NonNull AudioManager audioManager, + int audioStreamType, + @NonNull OnAudioVolumeChangedListener listener) { - super(handler); - mAudioManager = audioManager; - mAudioStreamType = audioStreamType; - mListener = listener; - mLastVolume = audioManager.getStreamVolume(mAudioStreamType); + super(handler); + mAudioManager = audioManager; + mAudioStreamType = audioStreamType; + mListener = listener; + mLastVolume = audioManager.getStreamVolume(mAudioStreamType); + } + + /** Depending on the handler this method may be executed on the UI thread */ + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mAudioManager != null && mListener != null) { + int maxVolume = mAudioManager.getStreamMaxVolume(mAudioStreamType); + int currentVolume = mAudioManager.getStreamVolume(mAudioStreamType); + if (currentVolume != mLastVolume) { + mLastVolume = currentVolume; + mListener.onAudioVolumeChanged(currentVolume, maxVolume); + } } + } - /** - * Depending on the handler this method may be executed on the UI thread - */ - @Override - public void onChange(boolean selfChange, Uri uri) { - if (mAudioManager != null && mListener != null) { - int maxVolume = mAudioManager.getStreamMaxVolume(mAudioStreamType); - int currentVolume = mAudioManager.getStreamVolume(mAudioStreamType); - if (currentVolume != mLastVolume) { - mLastVolume = currentVolume; - mListener.onAudioVolumeChanged(currentVolume, maxVolume); - } - } - } - - @Override - public boolean deliverSelfNotifications() { - return super.deliverSelfNotifications(); - } -} \ No newline at end of file + @Override + public boolean deliverSelfNotifications() { + return super.deliverSelfNotifications(); + } +} diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index ad6626bff..80614a59e 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -15,7 +15,7 @@ app:argType="code.name.monkey.retromusic.model.Genre" /> - - + --> Date: Tue, 6 Oct 2020 14:36:16 +0530 Subject: [PATCH 41/72] Updated Spotless --- .../playlists/PlaylistDetailsFragment.kt | 3 +-- app/src/main/res/navigation/main_graph.xml | 4 ++-- build.gradle | 3 ++- spotless.gradle | 17 ++--------------- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt index 6637984b4..dc3e874c8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt @@ -24,7 +24,7 @@ import org.koin.core.parameter.parametersOf class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_detail) { private val arguments by navArgs() - private val viewModel: PlaylistDetailsViewModel by viewModel { + private val viewModel by viewModel { parametersOf(arguments.extraPlaylist) } @@ -89,7 +89,6 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli emptyText.isVisible = playlistSongAdapter.itemCount == 0 } - override fun onDestroy() { recyclerView?.itemAnimator = null recyclerView?.adapter = null diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 80614a59e..ad6626bff 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -15,7 +15,7 @@ app:argType="code.name.monkey.retromusic.model.Genre" /> - + Date: Tue, 6 Oct 2020 14:37:45 +0530 Subject: [PATCH 42/72] Add XML rules --- app/build.gradle | 2 +- spotless.gradle | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c1eb4656a..0b3ba9096 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -168,4 +168,4 @@ dependencies { debugImplementation 'com.amitshekhar.android:debug-db:1.0.6' } -apply from: '../spotless.gradle' \ No newline at end of file +apply from: '../spotless.gradle' diff --git a/spotless.gradle b/spotless.gradle index 94fb10f5f..c23417c96 100644 --- a/spotless.gradle +++ b/spotless.gradle @@ -14,4 +14,17 @@ spotless { trimTrailingWhitespace() endWithNewline() } + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } + + format 'xml', { + target 'src/*.xml' + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } } \ No newline at end of file From e5f6e83dd49b561ce90564f6bf54e9b94642ca60 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 9 Oct 2020 01:14:10 +0530 Subject: [PATCH 43/72] Fix playlist reload --- .../dialogs/CreatePlaylistDialog.kt | 13 +--- .../retromusic/fragments/LibraryViewModel.kt | 49 +++++++++++-- .../res/layout-xlarge-land/fragment_blur.xml | 71 ++++++++++--------- app/src/main/res/values/styles.xml | 6 ++ build.gradle | 3 +- 5 files changed, 90 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt index 052e3a1d8..b4bcf8469 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/CreatePlaylistDialog.kt @@ -68,17 +68,8 @@ class CreatePlaylistDialog : DialogFragment() { ) { _, _ -> val playlistName = playlistView.text.toString() if (!TextUtils.isEmpty(playlistName)) { - lifecycleScope.launch(Dispatchers.IO) { - if (libraryViewModel.checkPlaylistExists(playlistName).isEmpty()) { - val playlistId: Long = - libraryViewModel.createPlaylist(PlaylistEntity(playlistName = playlistName)) - libraryViewModel.insertSongs(songs.map { it.toSongEntity(playlistId) }) - libraryViewModel.forceReload(Playlists) - } else { - Toast.makeText(requireContext(), "Playlist exists", Toast.LENGTH_SHORT) - .show() - } - } + libraryViewModel.addToPlaylist(playlistName, songs) + } else { playlistContainer.error = "Playlist is can't be empty" } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 6a5548548..5854a750b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -14,16 +14,32 @@ */ package code.name.monkey.retromusic.fragments -import androidx.lifecycle.* +import android.widget.Toast +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope +import code.name.monkey.retromusic.App import code.name.monkey.retromusic.RECENT_ALBUMS import code.name.monkey.retromusic.RECENT_ARTISTS import code.name.monkey.retromusic.TOP_ALBUMS import code.name.monkey.retromusic.TOP_ARTISTS -import code.name.monkey.retromusic.db.* +import code.name.monkey.retromusic.db.PlaylistEntity +import code.name.monkey.retromusic.db.PlaylistWithSongs +import code.name.monkey.retromusic.db.SongEntity +import code.name.monkey.retromusic.db.toSong +import code.name.monkey.retromusic.db.toSongEntity import code.name.monkey.retromusic.fragments.ReloadType.* import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener -import code.name.monkey.retromusic.model.* +import code.name.monkey.retromusic.model.Album +import code.name.monkey.retromusic.model.Artist +import code.name.monkey.retromusic.model.Contributor +import code.name.monkey.retromusic.model.Genre +import code.name.monkey.retromusic.model.Home +import code.name.monkey.retromusic.model.Playlist +import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.RealRepository import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.PreferenceUtil @@ -226,10 +242,10 @@ class LibraryViewModel( suspend fun removeSongFromPlaylist(songEntity: SongEntity) = repository.removeSongFromPlaylist(songEntity) - suspend fun checkPlaylistExists(playlistName: String): List = + private suspend fun checkPlaylistExists(playlistName: String): List = repository.checkPlaylistExists(playlistName) - suspend fun createPlaylist(playlistEntity: PlaylistEntity): Long = + private suspend fun createPlaylist(playlistEntity: PlaylistEntity): Long = repository.createPlaylist(playlistEntity) fun importPlaylists() = viewModelScope.launch(IO) { @@ -303,6 +319,29 @@ class LibraryViewModel( searchResults.postValue(emptyList()) } } + + fun addToPlaylist(playlistName: String, songs: List) { + viewModelScope.launch(IO) { + val playlists = checkPlaylistExists(playlistName) + if (playlists.isEmpty()) { + val playlistId: Long = createPlaylist(PlaylistEntity(playlistName = playlistName)) + insertSongs(songs.map { it.toSongEntity(playlistId) }) + forceReload(Playlists) + } else { + val playlist = playlists.firstOrNull() + if (playlist != null) { + insertSongs(songs.map { + it.toSongEntity(playListId = playlist.playListId) + }) + } + Toast.makeText( + App.getContext(), + "Adding songs to $playlistName", + Toast.LENGTH_SHORT + ).show() + } + } + } } enum class ReloadType { diff --git a/app/src/main/res/layout-xlarge-land/fragment_blur.xml b/app/src/main/res/layout-xlarge-land/fragment_blur.xml index a2185426d..c918471ca 100644 --- a/app/src/main/res/layout-xlarge-land/fragment_blur.xml +++ b/app/src/main/res/layout-xlarge-land/fragment_blur.xml @@ -16,54 +16,57 @@ android:scaleType="centerCrop" app:srcCompat="@color/black_color" /> - - - + + - - + - + tools:layout="@layout/fragment_album_cover" /> + - - - + + + + ?android:attr/colorButtonNormal 0dp + + + diff --git a/build.gradle b/build.gradle index 234550017..c6c313625 100644 --- a/build.gradle +++ b/build.gradle @@ -7,11 +7,10 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" def nav_version = "2.3.0" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" - //noinspection GradleDependency classpath "com.diffplug.spotless:spotless-plugin-gradle:4.5.1" } } From 96cd8de8963f98f18a0aeddc3283b6ca52575e26 Mon Sep 17 00:00:00 2001 From: "Daksh P. Jain" Date: Fri, 9 Oct 2020 09:34:55 +0530 Subject: [PATCH 44/72] Update app/src/main/res/values-es-rES/strings.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anyelo Almánzar Mercedes <22483347+anyeloamt@users.noreply.github.com> --- app/src/main/res/values-es-rES/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 4ae9f8692..6a4ae7473 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -230,7 +230,7 @@ No hay Álbumes No hay Artistas "Primero reproduce una canción, luego intenta de nuevo." - No se encontró nigún ecualizador + No se encontró ningún ecualizador No hay Géneros No se encontró la letra No hay canciones tocando From a4bc5f6ca75413d15531fcc56537ad9fb9a0ac17 Mon Sep 17 00:00:00 2001 From: dylan <56566724+d-l-n@users.noreply.github.com> Date: Fri, 9 Oct 2020 02:31:53 -0300 Subject: [PATCH 45/72] Update strings.xml Fixed conflicts with https://github.com/h4h13/RetroMusicPlayer/compare/Daksh777-patch-1 --- app/src/main/res/values-es-rES/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 6a4ae7473..029c06d4f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -511,12 +511,12 @@ Iniciar pago Mostrar en pantalla de reproducción Al hacer clic en la notificación se mostrará la pantalla de reproducción en lugar de la pantalla de inicio - Carta pequeña + Tarjeta pequeña Acerca de %s - Seleccionar idioma + Seleccionar lenguaje Traductores - La gente que ayudó a traductir esta app - Prueba Retro Music Premium + Las personas que ayudaron a traducir esta aplicación + Pruebe Retro Music Premium Canción Canciones From 116249c38f3f733a8e1414ffc9d4d0f390b2a3f0 Mon Sep 17 00:00:00 2001 From: "Daksh P. Jain" Date: Fri, 9 Oct 2020 13:36:49 +0530 Subject: [PATCH 46/72] Update strings.xml --- app/src/main/res/values-es-rES/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 029c06d4f..3c96555bd 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -199,7 +199,7 @@ Nombre del archivo Ruta del archivo Tamaño - Más desde %s + Más de %s Frecuencia de muestreo Duración Etiquetado From 2e9cc963f97e7c9e079bc769f7c0b163966ad3b1 Mon Sep 17 00:00:00 2001 From: "Daksh P. Jain" Date: Fri, 9 Oct 2020 13:46:14 +0530 Subject: [PATCH 47/72] Update strings.xml --- app/src/main/res/values-es-rES/strings.xml | 100 ++++++++++----------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index c58a3e9e0..3c96555bd 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -51,7 +51,7 @@ Añadir letra Agregar foto "Agregar a lista de reproducción" - Añadir retardo para letras + Añadir retraso para letras "Se ha agregado 1 canción a la cola de reproducción" %1$d canciones agregadas a la cola de reproducción Álbum @@ -74,7 +74,7 @@ Automático Color base del tema Refuerzo de graves - Biografía + Bio Biografía Negro Lista Negra @@ -82,7 +82,7 @@ Tarjeta con desenfoque Enviando el reporte a GitHub... Token de acceso inválido. Por favor, contacta con el desarrollador de la aplicación - El problema no esta habilitado para el repositorio seleccionado. Por favor, contacta col el desarrollador de la app. + El problema no está habilitado para el repositorio seleccionado. Por favor, contacta con el desarrollador de la app. Se ha producido un error inesperado. Por favor, contacta con el desarrollador de la aplicación Usuario o contraseña incorrectos Problema @@ -183,7 +183,7 @@ 6 7 8 - Cuadricula y estilo + Cuadrícula y estilo Giro Historial Inicio @@ -199,7 +199,7 @@ Nombre del archivo Ruta del archivo Tamaño - Más desde %s + Más de %s Frecuencia de muestreo Duración Etiquetado @@ -207,10 +207,10 @@ Última canción Vamos a tocar un poco de música Biblioteca - Categorías biblioteca + Categorías de la Biblioteca Licencias Blanco claro - Escuchadores + Oyentes Listando archivos Cargando productos... Iniciar Sección @@ -230,9 +230,9 @@ No hay Álbumes No hay Artistas "Primero reproduce una canción, luego intenta de nuevo." - No se encontró ecualizador + No se encontró ningún ecualizador No hay Géneros - No se encontró letra + No se encontró la letra No hay canciones tocando No hay Listas de Reproducción No se encontraron compras. @@ -256,7 +256,7 @@ Contraseña Más de 3 meses Pegar letra aquí - Peak + Pica Permiso de acceso al almacenamiento externo denegado. Permiso denegado. Personalizar @@ -289,7 +289,7 @@ Biblioteca Pantalla de bloqueo Listas de reproducción - Pausar la reproducción cuando se esta en silencio y reproducir cuando se aumenta el volumen. ¡Cuidado! Cuando se aumenta el volumen se empezara la reproducción aunque se este fuera de la app. + Pausar la reproducción cuando se está en silencio y reproducir cuando se aumenta el volumen. ¡Cuidado! Cuando se aumenta el volumen se empezará la reproducción aunque se esté fuera de la app. Pausar en cero Tenga en cuenta que habilitar esta función puede afectar la duración de la batería Mantener la pantalla encendida @@ -306,14 +306,14 @@ El color del fondo y los botones de control cambian de acuerdo a la portada del álbum para la ventana de reproducción Colorea los accesos directos de la aplicación en el color de énfasis. Cada vez que cambie el color, active esta opción Colorea la barra de navegación con el color principal - "Colorea la notificaci\u00f3n con el color vibrante de la portada del \u00e1lbum" + "Colorea la notificación con el color vibrante de la portada del álbum" Según las líneas de la guía Material Design en los colores del modo oscuro deben ser desaturados Se tomará el color dominante de la portada del álbum o imagen del artista Añadir controles extra al mini reproductor - Mostrar información extra de canciones, como el formato de archivo, bitrate y frecuencia + Mostrar información extra de canciones, como el formato de archivo, tasa de bits y frecuencia "Puede causar problemas de reproducción en algunos dispositivos" - Mostrar/Ocultar pestaña de géneros - Mostrar/Ocultar banner en inicio + Mostrar/Ocultar pestaña Géneros + Mostrar/Ocultar banner en Inicio Puede aumentar la calidad de la portada del álbum, pero provoca tiempos de carga de imágenes más lentos. Solo habilite esto si tiene problemas con portadas de baja resolución Configure la visibilidad y el orden de las categorías de la biblioteca. Usar los controles personalizados de Retro Music en la pantalla de bloqueo @@ -321,32 +321,32 @@ Redondear las esquinas de la aplicación Mostrar/Ocultar nombres de las pestañas de navegación Modo inmersivo - Comenzar a reproducir inmediatamente se conecten audífonos + Comenzar a reproducir inmediatamente cuando se conecten audífonos El modo aleatorio se desactivará cuando se reproduzca una nueva lista de canciones Mostrar controles de volumen si hay suficiente espacio disponible. Mostrar/Ocultar portada del álbum Tema de la portada del álbum Estilo de portada del álbum en reproducción - Cuadricula del álbum + Cuadrícula del álbum Accesos directos de la aplicación coloreados - Cuadricula de los artistas - Reducir el volumen en pérdida de enfoque + Cuadrícula de los artistas + Reducir el volumen cuando se pierda el enfoque Descarga automática de imágenes de artistas Lista Negra Reproducción por Bluetooth Desenfocar portada del álbum Elegir ecualizador Diseño de notificación clásico - Color adaptativo + Color Adaptativo Notificación coloreada Color Desaturado Controles extra Información de la canción Reproducción sin pausas Tema de la aplicación - Mostrar pestaña de géneros - Cuadricula de los artistas en inicio - Banner de inicio + Mostrar pestaña Géneros + Cuadrícula de los artistas en inicio + Banner de Inicio Ignorar las portadas de la biblioteca de medios Intervalo de la lista \"Añadidos Recientemente\" Controles en pantalla completa @@ -355,7 +355,7 @@ Licencias de código abierto Bordes de las esquinas Forma de los títulos de las pestañas - Efecto carrusel + Efecto Carrusel Color dominante Aplicación en pantalla completa Títulos de las pestañas @@ -364,7 +364,7 @@ Controles de volumen Información de usuario Color principal - El color principal del tema, por defecto es gris azulado, por ahora funciona con colores oscuros + El color principal del tema, por defecto gris azulado, por ahora funciona con colores oscuros Pro Temas en reproducción, efecto Carrusel, tema de color y más ... Perfil @@ -378,7 +378,7 @@ Eliminar Eliminar foto del banner Eliminar portada - Eliminar de la lista negra + Eliminar de la Lista Negra Eliminar foto de perfil Eliminar canción de la lista %1$s de la lista?]]> @@ -393,8 +393,8 @@ Compra anterior restaurada. Por favor, reinicie la aplicación para hacer uso de todas las funciones. Compras anteriores restauradas. Restaurando compra... - Ecualizador de Reto Music - Reproductor de Música Retro + Ecualizador de Retro Music + Reproductor de Retro Music Retro Music Pro La eliminación del archivo falló @@ -471,12 +471,12 @@ Tablero ¡Buenas Tardes! ¡Buen Día! - ¡Buenas Tardes! - ¡Buenos días! - Buenas noches + ¡Buenas Noches! + ¡Buen Día! + ¡Buenas Noches! ¿Cómo te llamas? Hoy - Álbumes mas reproducidos + Álbumes más reproducidos Artistas más reproducidos "Pista (2 para pista 2 o 3004 para CD3 pista 4)" Número de pista @@ -485,7 +485,7 @@ Twitter Comparte tu diseño con Retro Music Sin etiqueta - No se pudo reproducir esta canci\u00f3n + No se pudo reproducir esta canción A continuación Actualizar imagen Actualizando... @@ -504,37 +504,37 @@ %1$d seleccionados Año Tienes que seleccionar al menos una categoría - Sera redirigido al sitio web para reportar problemas. + Será redirigido al sitio web para reportar problemas. Los datos de tu cuenta sólo se utilizan para la autenticación Cantidad Nota (Opcional) Iniciar pago Mostrar en pantalla de reproducción Al hacer clic en la notificación se mostrará la pantalla de reproducción en lugar de la pantalla de inicio - Tiny card - About %s - Select language - Translators - The people who helped translate this app - Try Retro Music Premium + Tarjeta pequeña + Acerca de %s + Seleccionar lenguaje + Traductores + Las personas que ayudaron a traducir esta aplicación + Pruebe Retro Music Premium - Song - Songs + Canción + Canciones - Album - Albums + Álbum + Álbumes - %d Song - %d Songs + %d Canción + %d Canciones - %d Album - %d Albums + %d Álbum + %d Álbumes - %d Artist - %d Artists + %d Artista + %d Artistas From 33f4f31066cd728a77a2fd2a6e06ceb9efba825b Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Fri, 9 Oct 2020 23:14:02 +0530 Subject: [PATCH 48/72] Updated Playlist refresh Bottom navigation shows when coming from notification Removed animation for lyrics page Added playlist reorder Added refresh for album & artist details when update Fix when scan to update library Added sort for playlist Fix album art not showing in lockscreen --- app/src/main/ic_launcher-playstore.png | Bin 22855 -> 14330 bytes .../code/name/monkey/retromusic/Constants.kt | 1 + .../retromusic/activities/LyricsActivity.kt | 13 +- .../retromusic/activities/MainActivity.kt | 30 +- .../base/AbsSlidingMusicPanelActivity.kt | 10 +- .../adapter/album/AlbumCoverPagerAdapter.kt | 15 +- .../adapter/playlist/PlaylistAdapter.kt | 14 +- .../dialogs/RemoveSongFromPlaylistDialog.kt | 2 - .../retromusic/fragments/LibraryViewModel.kt | 7 +- .../fragments/albums/AlbumDetailsViewModel.kt | 17 +- .../artists/ArtistDetailsFragment.kt | 4 +- .../artists/ArtistDetailsViewModel.kt | 19 +- .../AbsRecyclerViewCustomGridSizeFragment.kt | 1 + .../fragments/base/AbsRecyclerViewFragment.kt | 1 + .../fragments/folder/FoldersFragment.java | 1420 ++++----- .../retromusic/fragments/home/HomeFragment.kt | 1 + .../playlists/PlaylistDetailsFragment.kt | 20 +- .../playlists/PlaylistDetailsViewModel.kt | 21 +- .../fragments/playlists/PlaylistsFragment.kt | 140 +- .../monkey/retromusic/helper/SortOrder.kt | 21 + .../interfaces/IPlaylistClickListener.kt | 8 + .../retromusic/repository/GenreRepository.kt | 39 +- .../retromusic/repository/RoomRepository.kt | 35 +- .../retromusic/service/MusicService.java | 2640 ++++++++--------- .../monkey/retromusic/util/PreferenceUtil.kt | 90 +- .../drawable-night/ic_launcher_background.xml | 10 + .../drawable-v24/ic_launcher_foreground.xml | 133 +- .../res/drawable/ic_launcher_background.xml | 2 +- app/src/main/res/layout/activity_lyrics.xml | 1 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3517 -> 3323 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 14004 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 2256 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3517 -> 3323 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2205 -> 2204 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 8211 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 1394 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2205 -> 2204 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 5189 -> 4613 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 20448 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 3273 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 5189 -> 4613 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 8281 -> 7157 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 34620 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 5716 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 8281 -> 7157 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 11968 -> 10135 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 50987 -> 0 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 8527 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 11968 -> 10135 bytes .../res/values/ic_launcher_background.xml | 2 +- app/src/main/res/values/ids.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/pref_ui.xml | 4 +- 55 files changed, 2532 insertions(+), 2199 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/interfaces/IPlaylistClickListener.kt create mode 100644 app/src/main/res/drawable-night/ic_launcher_background.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 9fdd702524da1c8b0b57e5f44cef3f4892fa3908..7730a4738f55a5fe35ff37680595e35a3e07d6d6 100644 GIT binary patch literal 14330 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 V3dS@_LI#w)9B}u+J=zm`@n59HOz!{y diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 452b2fa3f..ae1f1da75 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -108,6 +108,7 @@ const val INITIALIZED_BLACKLIST = "initialized_blacklist" const val ARTIST_SORT_ORDER = "artist_sort_order" const val ARTIST_ALBUM_SORT_ORDER = "artist_album_sort_order" const val ALBUM_SORT_ORDER = "album_sort_order" +const val PLAYLIST_SORT_ORDER = "playlist_sort_order" const val ALBUM_SONG_SORT_ORDER = "album_song_sort_order" const val ARTIST_SONG_SORT_ORDER = "artist_song_sort_order" const val ALBUM_GRID_SIZE = "album_grid_size" diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt index 25300b92a..ac4346223 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LyricsActivity.kt @@ -17,8 +17,8 @@ package code.name.monkey.retromusic.activities import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View import android.view.WindowManager +import androidx.core.view.ViewCompat import androidx.interpolator.view.animation.FastOutSlowInInterpolator import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper @@ -54,22 +54,17 @@ class LyricsActivity : AbsMusicServiceActivity(), MusicProgressViewUpdateHelper. private fun buildContainerTransform(): MaterialContainerTransform { val transform = MaterialContainerTransform() transform.setAllContainerColors( - MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface) + MaterialColors.getColor(findViewById(R.id.container), R.attr.colorSurface) ) - transform.addTarget(android.R.id.content) + transform.addTarget(R.id.container) transform.duration = 300 - transform.interpolator = FastOutSlowInInterpolator() - transform.pathMotion = MaterialArcMotion() return transform } override fun onCreate(savedInstanceState: Bundle?) { - findViewById(android.R.id.content).transitionName = "lyrics" - setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) - window.sharedElementEnterTransition = buildContainerTransform() - window.sharedElementReturnTransition = buildContainerTransform() super.onCreate(savedInstanceState) setContentView(R.layout.activity_lyrics) + ViewCompat.setTransitionName(container, "lyrics") setStatusbarColorAuto() setTaskDescriptionColorAuto() setNavigationbarColorAuto() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index eeb6426e7..aaf52166e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -23,7 +23,32 @@ import android.provider.MediaStore import android.view.View import androidx.lifecycle.lifecycleScope import androidx.navigation.ui.NavigationUI -import code.name.monkey.retromusic.* +import code.name.monkey.retromusic.ADAPTIVE_COLOR_APP +import code.name.monkey.retromusic.ALBUM_COVER_STYLE +import code.name.monkey.retromusic.ALBUM_COVER_TRANSFORM +import code.name.monkey.retromusic.BANNER_IMAGE_PATH +import code.name.monkey.retromusic.BLACK_THEME +import code.name.monkey.retromusic.CAROUSEL_EFFECT +import code.name.monkey.retromusic.CIRCULAR_ALBUM_ART +import code.name.monkey.retromusic.DESATURATED_COLOR +import code.name.monkey.retromusic.EXTRA_SONG_INFO +import code.name.monkey.retromusic.GENERAL_THEME +import code.name.monkey.retromusic.HOME_ARTIST_GRID_STYLE +import code.name.monkey.retromusic.KEEP_SCREEN_ON +import code.name.monkey.retromusic.LANGUAGE_NAME +import code.name.monkey.retromusic.LIBRARY_CATEGORIES +import code.name.monkey.retromusic.NOW_PLAYING_SCREEN_ID +import code.name.monkey.retromusic.PROFILE_IMAGE_PATH +import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.ROUND_CORNERS +import code.name.monkey.retromusic.TAB_TEXT_MODE +import code.name.monkey.retromusic.TOGGLE_ADD_CONTROLS +import code.name.monkey.retromusic.TOGGLE_FULL_SCREEN +import code.name.monkey.retromusic.TOGGLE_GENRE +import code.name.monkey.retromusic.TOGGLE_HOME_BANNER +import code.name.monkey.retromusic.TOGGLE_SEPARATE_LINE +import code.name.monkey.retromusic.TOGGLE_VOLUME +import code.name.monkey.retromusic.USER_NAME import code.name.monkey.retromusic.activities.base.AbsSlidingMusicPanelActivity import code.name.monkey.retromusic.extensions.findNavController import code.name.monkey.retromusic.helper.MusicPlayerRemote @@ -32,6 +57,7 @@ import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.PlaylistSongsLoader import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.AppRater import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.coroutines.Dispatchers.IO @@ -94,7 +120,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis intent.getBooleanExtra(EXPAND_PANEL, false) && PreferenceUtil.isExpandPanel ) { - expandPanel() + libraryViewModel.setPanelState(NowPlayingPanelState.EXPAND) intent.removeExtra(EXPAND_PANEL) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 009912afc..c6d7e5012 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -29,7 +29,11 @@ import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior -import code.name.monkey.retromusic.extensions.* +import code.name.monkey.retromusic.extensions.hide +import code.name.monkey.retromusic.extensions.peekHeightAnimate +import code.name.monkey.retromusic.extensions.show +import code.name.monkey.retromusic.extensions.translateXAnimate +import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment import code.name.monkey.retromusic.fragments.NowPlayingScreen @@ -118,6 +122,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { val themeColor = ATHUtil.resolveColor(this, android.R.attr.windowBackground, Color.GRAY) dimBackground.setBackgroundColor(ColorUtil.withAlpha(themeColor, 0.5f)) dimBackground.setOnClickListener { + println("dimBackground") libraryViewModel.setPanelState(COLLAPSED_WITH) } } @@ -209,6 +214,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this) + println("onServiceConnected") if (bottomNavigationView.isVisible) { libraryViewModel.setPanelState(COLLAPSED_WITH) } else { @@ -225,6 +231,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { libraryViewModel.setPanelState(HIDE) } else { if (bottomNavigationView.isVisible) { + println("onQueueChanged") libraryViewModel.setPanelState(COLLAPSED_WITH) } else { libraryViewModel.setPanelState(COLLAPSED_WITHOUT) @@ -339,6 +346,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { EXPAND -> { println("EXPAND") expandPanel() + bottomNavigationView.translateXAnimate(150f) } HIDE -> { println("HIDE") diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt index d994c8243..f6ca41790 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt @@ -14,18 +14,16 @@ */ package code.name.monkey.retromusic.adapter.album -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView -import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.activities.LyricsActivity import code.name.monkey.retromusic.fragments.AlbumCoverStyle import code.name.monkey.retromusic.fragments.NowPlayingScreen.* import code.name.monkey.retromusic.glide.RetroMusicColoredTarget @@ -105,15 +103,10 @@ class AlbumCoverPagerAdapter( savedInstanceState: Bundle? ): View? { val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false) + ViewCompat.setTransitionName(view, "lyrics") albumCover = view.findViewById(R.id.player_image) - albumCover.setOnClickListener { - val intent = Intent(requireContext(), LyricsActivity::class.java) - val activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), - it, - "lyrics" - ) - startActivity(intent, activityOptions.toBundle()) + view.setOnClickListener { + showLyricsDialog() } return view } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt index ddfe8c0c6..df14d37d1 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt @@ -24,12 +24,10 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu -import androidx.core.os.bundleOf +import androidx.core.view.ViewCompat import androidx.fragment.app.FragmentActivity -import androidx.navigation.findNavController import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.TintHelper -import code.name.monkey.retromusic.EXTRA_PLAYLIST import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder @@ -42,6 +40,7 @@ import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.helper.menu.SongsMenuHelper import code.name.monkey.retromusic.interfaces.ICabHolder +import code.name.monkey.retromusic.interfaces.IPlaylistClickListener import code.name.monkey.retromusic.model.Playlist import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.repository.PlaylistSongsLoader @@ -53,7 +52,8 @@ class PlaylistAdapter( private val activity: FragmentActivity, var dataSet: List, private var itemLayoutRes: Int, - ICabHolder: ICabHolder? + ICabHolder: ICabHolder?, + private val listener: IPlaylistClickListener ) : AbsMultiSelectAdapter( activity, ICabHolder, @@ -172,10 +172,8 @@ class PlaylistAdapter( if (isInQuickSelectMode) { toggleChecked(layoutPosition) } else { - activity.findNavController(R.id.fragment_container).navigate( - R.id.playlistDetailsFragment, - bundleOf(EXTRA_PLAYLIST to dataSet[layoutPosition]) - ) + ViewCompat.setTransitionName(itemView, "playlist") + listener.onPlaylistClick(dataSet[layoutPosition], itemView) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt index 93ac92f85..a480c7a33 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/RemoveSongFromPlaylistDialog.kt @@ -26,7 +26,6 @@ import code.name.monkey.retromusic.extensions.colorButtons import code.name.monkey.retromusic.extensions.extraNotNull import code.name.monkey.retromusic.extensions.materialDialog import code.name.monkey.retromusic.fragments.LibraryViewModel -import code.name.monkey.retromusic.fragments.ReloadType.Playlists import org.koin.androidx.viewmodel.ext.android.sharedViewModel class RemoveSongFromPlaylistDialog : DialogFragment() { @@ -74,7 +73,6 @@ class RemoveSongFromPlaylistDialog : DialogFragment() { .setMessage(pair.second) .setPositiveButton(R.string.remove_action) { _, _ -> libraryViewModel.deleteSongsInPlaylist(songs) - libraryViewModel.forceReload(Playlists) } .setNegativeButton(android.R.string.cancel, null) .create() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 5854a750b..96b28733e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -222,8 +222,11 @@ class LibraryViewModel( repository.renameRoomPlaylist(playListId, name) } - fun deleteSongsInPlaylist(songs: List) = viewModelScope.launch(IO) { - repository.deleteSongsInPlaylist(songs) + fun deleteSongsInPlaylist(songs: List) { + viewModelScope.launch(IO) { + repository.deleteSongsInPlaylist(songs) + forceReload(Playlists) + } } fun deleteSongsFromPlaylist(playlists: List) = viewModelScope.launch(IO) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt index c3f85d727..0dfa0ba7a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsViewModel.kt @@ -15,8 +15,10 @@ package code.name.monkey.retromusic.fragments.albums import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist @@ -24,16 +26,26 @@ import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmAlbum import code.name.monkey.retromusic.repository.RealRepository import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch class AlbumDetailsViewModel( private val repository: RealRepository, private val albumId: Long ) : ViewModel(), IMusicServiceEventListener { + private val albumDetails = MutableLiveData() - fun getAlbum(): LiveData = liveData(IO) { - emit(repository.albumByIdAsync(albumId)) + init { + fetchAlbum() } + private fun fetchAlbum() { + viewModelScope.launch(IO) { + albumDetails.postValue(repository.albumByIdAsync(albumId)) + } + } + + fun getAlbum(): LiveData = albumDetails + fun getArtist(artistId: Long): LiveData = liveData(IO) { val artist = repository.artistById(artistId) emit(artist) @@ -51,6 +63,7 @@ class AlbumDetailsViewModel( } override fun onMediaStoreChanged() { + fetchAlbum() } override fun onServiceConnected() {} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt index d63b0e1b9..c9f12a702 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt @@ -94,16 +94,14 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) - mainActivity.setSupportActionBar(toolbar) + libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) toolbar.title = null setupRecyclerView() ViewCompat.setTransitionName(container, "artist") - postponeEnterTransition() detailsViewModel.getArtist().observe(viewLifecycleOwner, Observer { startPostponedEnterTransition() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt index a871069f8..dbb59866b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsViewModel.kt @@ -15,25 +15,36 @@ package code.name.monkey.retromusic.fragments.artists import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmArtist import code.name.monkey.retromusic.repository.RealRepository import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch class ArtistDetailsViewModel( private val realRepository: RealRepository, private val artistId: Long ) : ViewModel(), IMusicServiceEventListener { + private val artistDetails = MutableLiveData() - fun getArtist(): LiveData = liveData(IO) { - val artist = realRepository.artistById(artistId) - emit(artist) + init { + fetchArtist() } + private fun fetchArtist() { + viewModelScope.launch(IO) { + artistDetails.postValue(realRepository.artistById(artistId)) + } + } + + fun getArtist(): LiveData = artistDetails + fun getArtistInfo( name: String, lang: String?, @@ -45,7 +56,7 @@ class ArtistDetailsViewModel( } override fun onMediaStoreChanged() { - getArtist() + fetchArtist() } override fun onServiceConnected() {} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewCustomGridSizeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewCustomGridSizeFragment.kt index cf46337db..7a03ab2a2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewCustomGridSizeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewCustomGridSizeFragment.kt @@ -73,6 +73,7 @@ abstract class AbsRecyclerViewCustomGridSizeFragment fun setAndSaveSortOrder(sortOrder: String) { this.sortOrder = sortOrder + println(sortOrder) saveSortOrder(sortOrder) setSortOrder(sortOrder) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt index 1d06263b5..86243a13c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt @@ -61,6 +61,7 @@ abstract class AbsRecyclerViewFragment, LM : Recycle override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + println("AbsRecyclerViewFragment") libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index e0769e8fd..5e03ad767 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -14,8 +14,6 @@ package code.name.monkey.retromusic.fragments.folder; -import static code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor; - import android.app.Dialog; import android.content.Context; import android.media.MediaScannerConnection; @@ -34,6 +32,7 @@ import android.webkit.MimeTypeMap; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; @@ -43,6 +42,23 @@ import androidx.loader.content.Loader; import androidx.navigation.Navigation; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + +import com.afollestad.materialcab.MaterialCab; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; @@ -67,754 +83,744 @@ import code.name.monkey.retromusic.util.RetroColorUtil; import code.name.monkey.retromusic.util.ThemedFastScroller; import code.name.monkey.retromusic.views.BreadCrumbLayout; import code.name.monkey.retromusic.views.ScrollingViewOnApplyWindowInsetsListener; -import com.afollestad.materialcab.MaterialCab; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; import me.zhanghai.android.fastscroll.FastScroller; -import org.jetbrains.annotations.NotNull; + +import static code.name.monkey.appthemehelper.common.ATHToolbarActivity.getToolbarBackgroundColor; public class FoldersFragment extends AbsMainActivityFragment - implements IMainActivityFragmentCallbacks, + implements IMainActivityFragmentCallbacks, ICabHolder, BreadCrumbLayout.SelectionCallback, ICallbacks, LoaderManager.LoaderCallbacks> { - public static final String TAG = FoldersFragment.class.getSimpleName(); - public static final FileFilter AUDIO_FILE_FILTER = - file -> - !file.isHidden() - && (file.isDirectory() - || FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()) - || FileUtil.fileIsMimeType(file, "application/opus", MimeTypeMap.getSingleton()) - || FileUtil.fileIsMimeType(file, "application/ogg", MimeTypeMap.getSingleton())); + public static final String TAG = FoldersFragment.class.getSimpleName(); + public static final FileFilter AUDIO_FILE_FILTER = + file -> + !file.isHidden() + && (file.isDirectory() + || FileUtil.fileIsMimeType(file, "audio/*", MimeTypeMap.getSingleton()) + || FileUtil.fileIsMimeType(file, "application/opus", MimeTypeMap.getSingleton()) + || FileUtil.fileIsMimeType(file, "application/ogg", MimeTypeMap.getSingleton())); - private static final String CRUMBS = "crumbs"; - private static final int LOADER_ID = 5; - private SongFileAdapter adapter; - private Toolbar toolbar; - private TextView appNameText; - private BreadCrumbLayout breadCrumbs; - private MaterialCab cab; - private View coordinatorLayout; - private View empty; - private TextView emojiText; - private Comparator fileComparator = - (lhs, rhs) -> { - if (lhs.isDirectory() && !rhs.isDirectory()) { - return -1; - } else if (!lhs.isDirectory() && rhs.isDirectory()) { - return 1; - } else { - return lhs.getName().compareToIgnoreCase(rhs.getName()); - } - }; - private RecyclerView recyclerView; - - public FoldersFragment() { - super(R.layout.fragment_folder); - } - - public static File getDefaultStartDirectory() { - File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); - File startFolder; - if (musicDir.exists() && musicDir.isDirectory()) { - startFolder = musicDir; - } else { - File externalStorage = Environment.getExternalStorageDirectory(); - if (externalStorage.exists() && externalStorage.isDirectory()) { - startFolder = externalStorage; - } else { - startFolder = new File("/"); // root - } - } - return startFolder; - } - - private static File tryGetCanonicalFile(File file) { - try { - return file.getCanonicalFile(); - } catch (IOException e) { - e.printStackTrace(); - return file; - } - } - - @NonNull - @Override - public View onCreateView( - @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_folder, container, false); - initViews(view); - return view; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); - getMainActivity().setSupportActionBar(toolbar); - getMainActivity().getSupportActionBar().setTitle(null); - setStatusBarColorAuto(view); - setUpAppbarColor(); - setUpBreadCrumbs(); - setUpRecyclerView(); - setUpAdapter(); - setUpTitle(); - } - - private void setUpTitle() { - toolbar.setNavigationOnClickListener( - v -> Navigation.findNavController(v).navigate(R.id.searchFragment, null, getNavOptions())); - int color = ThemeStore.Companion.accentColor(requireContext()); - String hexColor = String.format("#%06X", 0xFFFFFF & color); - Spanned appName = - HtmlCompat.fromHtml( - "Retro Music", - HtmlCompat.FROM_HTML_MODE_COMPACT); - appNameText.setText(appName); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - if (savedInstanceState == null) { - setCrumb( - new BreadCrumbLayout.Crumb( - FileUtil.safeGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), - true); - } else { - breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS)); - LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this); - } - } - - @Override - public void onPause() { - super.onPause(); - saveScrollPosition(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (breadCrumbs != null) { - outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper()); - } - } - - @Override - public boolean handleBackPress() { - if (cab != null && cab.isActive()) { - cab.finish(); - return true; - } - if (breadCrumbs != null && breadCrumbs.popHistory()) { - setCrumb(breadCrumbs.lastHistory(), false); - return true; - } - return false; - } - - @NonNull - @Override - public Loader> onCreateLoader(int id, Bundle args) { - return new AsyncFileLoader(this); - } - - @Override - public void onCrumbSelection(BreadCrumbLayout.Crumb crumb, int index) { - setCrumb(crumb, true); - } - - @Override - public void onFileMenuClicked(final File file, @NotNull View view) { - PopupMenu popupMenu = new PopupMenu(getActivity(), view); - if (file.isDirectory()) { - popupMenu.inflate(R.menu.menu_item_directory); - popupMenu.setOnMenuItemClickListener( - item -> { - final int itemId = item.getItemId(); - switch (itemId) { - case R.id.action_play_next: - case R.id.action_add_to_current_playing: - case R.id.action_add_to_playlist: - case R.id.action_delete_from_device: - new ListSongsAsyncTask( - getActivity(), - null, - (songs, extra) -> { - if (!songs.isEmpty()) { - SongsMenuHelper.INSTANCE.handleMenuClick( - requireActivity(), songs, itemId); - } - }) - .execute( - new ListSongsAsyncTask.LoadingInfo( - toList(file), AUDIO_FILE_FILTER, getFileComparator())); - return true; - case R.id.action_set_as_start_directory: - PreferenceUtil.INSTANCE.setStartDirectory(file); - Toast.makeText( - getActivity(), - String.format(getString(R.string.new_start_directory), file.getPath()), - Toast.LENGTH_SHORT) - .show(); - return true; - case R.id.action_scan: - new ListPathsAsyncTask(getActivity(), this::scanPaths) - .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); - return true; - } - return false; - }); - } else { - popupMenu.inflate(R.menu.menu_item_file); - popupMenu.setOnMenuItemClickListener( - item -> { - final int itemId = item.getItemId(); - switch (itemId) { - case R.id.action_play_next: - case R.id.action_add_to_current_playing: - case R.id.action_add_to_playlist: - case R.id.action_go_to_album: - case R.id.action_go_to_artist: - case R.id.action_share: - case R.id.action_tag_editor: - case R.id.action_details: - case R.id.action_set_as_ringtone: - case R.id.action_delete_from_device: - new ListSongsAsyncTask( - getActivity(), - null, - (songs, extra) -> - SongMenuHelper.INSTANCE.handleMenuClick( - requireActivity(), songs.get(0), itemId)) - .execute( - new ListSongsAsyncTask.LoadingInfo( - toList(file), AUDIO_FILE_FILTER, getFileComparator())); - return true; - case R.id.action_scan: - new ListPathsAsyncTask(getActivity(), this::scanPaths) - .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); - return true; - } - return false; - }); - } - popupMenu.show(); - } - - @Override - public void onFileSelected(@NotNull File file) { - file = tryGetCanonicalFile(file); // important as we compare the path value later - if (file.isDirectory()) { - setCrumb(new BreadCrumbLayout.Crumb(file), true); - } else { - FileFilter fileFilter = - pathname -> !pathname.isDirectory() && AUDIO_FILE_FILTER.accept(pathname); - new ListSongsAsyncTask( - getActivity(), - file, - (songs, extra) -> { - File file1 = (File) extra; - int startIndex = -1; - for (int i = 0; i < songs.size(); i++) { - if (file1 - .getPath() - .equals(songs.get(i).getData())) { // path is already canonical here - startIndex = i; - break; - } - } - if (startIndex > -1) { - MusicPlayerRemote.openQueue(songs, startIndex, true); + private static final String CRUMBS = "crumbs"; + private static final int LOADER_ID = 5; + private SongFileAdapter adapter; + private Toolbar toolbar; + private TextView appNameText; + private BreadCrumbLayout breadCrumbs; + private MaterialCab cab; + private View coordinatorLayout; + private View empty; + private TextView emojiText; + private Comparator fileComparator = + (lhs, rhs) -> { + if (lhs.isDirectory() && !rhs.isDirectory()) { + return -1; + } else if (!lhs.isDirectory() && rhs.isDirectory()) { + return 1; } else { - final File finalFile = file1; - Snackbar.make( - coordinatorLayout, - Html.fromHtml( - String.format( - getString(R.string.not_listed_in_media_store), file1.getName())), - Snackbar.LENGTH_LONG) - .setAction( - R.string.action_scan, - v -> - new ListPathsAsyncTask(requireActivity(), this::scanPaths) - .execute( - new ListPathsAsyncTask.LoadingInfo( - finalFile, AUDIO_FILE_FILTER))) - .setActionTextColor(ThemeStore.Companion.accentColor(requireActivity())) - .show(); + return lhs.getName().compareToIgnoreCase(rhs.getName()); } - }) - .execute( - new ListSongsAsyncTask.LoadingInfo( - toList(file.getParentFile()), fileFilter, getFileComparator())); + }; + private RecyclerView recyclerView; + + public FoldersFragment() { + super(R.layout.fragment_folder); } - } - @Override - public void onLoadFinished(@NonNull Loader> loader, List data) { - updateAdapter(data); - } + public static File getDefaultStartDirectory() { + File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); + File startFolder; + if (musicDir.exists() && musicDir.isDirectory()) { + startFolder = musicDir; + } else { + File externalStorage = Environment.getExternalStorageDirectory(); + if (externalStorage.exists() && externalStorage.isDirectory()) { + startFolder = externalStorage; + } else { + startFolder = new File("/"); // root + } + } + return startFolder; + } - @Override - public void onLoaderReset(@NonNull Loader> loader) { - updateAdapter(new LinkedList()); - } + private static File tryGetCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + e.printStackTrace(); + return file; + } + } - @Override - public void onMultipleItemAction(MenuItem item, @NotNull ArrayList files) { - final int itemId = item.getItemId(); - new ListSongsAsyncTask( - getActivity(), - null, - (songs, extra) -> - SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId)) - .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); - } + @NonNull + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_folder, container, false); + initViews(view); + return view; + } - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar); - } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getMainActivity().addMusicServiceEventListener(getLibraryViewModel()); + getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); + getMainActivity().setSupportActionBar(toolbar); + getMainActivity().getSupportActionBar().setTitle(null); + setStatusBarColorAuto(view); + setUpAppbarColor(); + setUpBreadCrumbs(); + setUpRecyclerView(); + setUpAdapter(); + setUpTitle(); + } - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - menu.add(0, R.id.action_scan, 0, R.string.scan_media) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - menu.removeItem(R.id.action_grid_size); - menu.removeItem(R.id.action_layout_type); - menu.removeItem(R.id.action_sort_order); - ToolbarContentTintHelper.handleOnCreateOptionsMenu( - requireContext(), toolbar, menu, getToolbarBackgroundColor(toolbar)); - } + private void setUpTitle() { + toolbar.setNavigationOnClickListener( + v -> Navigation.findNavController(v).navigate(R.id.searchFragment, null, getNavOptions())); + int color = ThemeStore.Companion.accentColor(requireContext()); + String hexColor = String.format("#%06X", 0xFFFFFF & color); + Spanned appName = + HtmlCompat.fromHtml( + "Retro Music", + HtmlCompat.FROM_HTML_MODE_COMPACT); + appNameText.setText(appName); + } - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.action_go_to_start_directory: - setCrumb( - new BreadCrumbLayout.Crumb( - tryGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), - true); - return true; - case R.id.action_scan: + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + if (savedInstanceState == null) { + setCrumb( + new BreadCrumbLayout.Crumb( + FileUtil.safeGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), + true); + } else { + breadCrumbs.restoreFromStateWrapper(savedInstanceState.getParcelable(CRUMBS)); + LoaderManager.getInstance(this).initLoader(LOADER_ID, null, this); + } + } + + @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (breadCrumbs != null) { + outState.putParcelable(CRUMBS, breadCrumbs.getStateWrapper()); + } + } + + @Override + public boolean handleBackPress() { + if (cab != null && cab.isActive()) { + cab.finish(); + return true; + } + if (breadCrumbs != null && breadCrumbs.popHistory()) { + setCrumb(breadCrumbs.lastHistory(), false); + return true; + } + return false; + } + + @NonNull + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new AsyncFileLoader(this); + } + + @Override + public void onCrumbSelection(BreadCrumbLayout.Crumb crumb, int index) { + setCrumb(crumb, true); + } + + @Override + public void onFileMenuClicked(final File file, @NotNull View view) { + PopupMenu popupMenu = new PopupMenu(getActivity(), view); + if (file.isDirectory()) { + popupMenu.inflate(R.menu.menu_item_directory); + popupMenu.setOnMenuItemClickListener( + item -> { + final int itemId = item.getItemId(); + switch (itemId) { + case R.id.action_play_next: + case R.id.action_add_to_current_playing: + case R.id.action_add_to_playlist: + case R.id.action_delete_from_device: + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> { + if (!songs.isEmpty()) { + SongsMenuHelper.INSTANCE.handleMenuClick( + requireActivity(), songs, itemId); + } + }) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file), AUDIO_FILE_FILTER, getFileComparator())); + return true; + case R.id.action_set_as_start_directory: + PreferenceUtil.INSTANCE.setStartDirectory(file); + Toast.makeText( + getActivity(), + String.format(getString(R.string.new_start_directory), file.getPath()), + Toast.LENGTH_SHORT) + .show(); + return true; + case R.id.action_scan: + new ListPathsAsyncTask(getActivity(), this::scanPaths) + .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); + return true; + } + return false; + }); + } else { + popupMenu.inflate(R.menu.menu_item_file); + popupMenu.setOnMenuItemClickListener( + item -> { + final int itemId = item.getItemId(); + switch (itemId) { + case R.id.action_play_next: + case R.id.action_add_to_current_playing: + case R.id.action_add_to_playlist: + case R.id.action_go_to_album: + case R.id.action_go_to_artist: + case R.id.action_share: + case R.id.action_tag_editor: + case R.id.action_details: + case R.id.action_set_as_ringtone: + case R.id.action_delete_from_device: + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> + SongMenuHelper.INSTANCE.handleMenuClick( + requireActivity(), songs.get(0), itemId)) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file), AUDIO_FILE_FILTER, getFileComparator())); + return true; + case R.id.action_scan: + new ListPathsAsyncTask(getActivity(), this::scanPaths) + .execute(new ListPathsAsyncTask.LoadingInfo(file, AUDIO_FILE_FILTER)); + return true; + } + return false; + }); + } + popupMenu.show(); + } + + @Override + public void onFileSelected(@NotNull File file) { + file = tryGetCanonicalFile(file); // important as we compare the path value later + if (file.isDirectory()) { + setCrumb(new BreadCrumbLayout.Crumb(file), true); + } else { + FileFilter fileFilter = + pathname -> !pathname.isDirectory() && AUDIO_FILE_FILTER.accept(pathname); + new ListSongsAsyncTask( + getActivity(), + file, + (songs, extra) -> { + File file1 = (File) extra; + int startIndex = -1; + for (int i = 0; i < songs.size(); i++) { + if (file1 + .getPath() + .equals(songs.get(i).getData())) { // path is already canonical here + startIndex = i; + break; + } + } + if (startIndex > -1) { + MusicPlayerRemote.openQueue(songs, startIndex, true); + } else { + final File finalFile = file1; + Snackbar.make( + coordinatorLayout, + Html.fromHtml( + String.format( + getString(R.string.not_listed_in_media_store), file1.getName())), + Snackbar.LENGTH_LONG) + .setAction( + R.string.action_scan, + v -> + new ListPathsAsyncTask(requireActivity(), this::scanPaths) + .execute( + new ListPathsAsyncTask.LoadingInfo( + finalFile, AUDIO_FILE_FILTER))) + .setActionTextColor(ThemeStore.Companion.accentColor(requireActivity())) + .show(); + } + }) + .execute( + new ListSongsAsyncTask.LoadingInfo( + toList(file.getParentFile()), fileFilter, getFileComparator())); + } + } + + @Override + public void onLoadFinished(@NonNull Loader> loader, List data) { + updateAdapter(data); + } + + @Override + public void onLoaderReset(@NonNull Loader> loader) { + updateAdapter(new LinkedList()); + } + + @Override + public void onMultipleItemAction(MenuItem item, @NotNull ArrayList files) { + final int itemId = item.getItemId(); + new ListSongsAsyncTask( + getActivity(), + null, + (songs, extra) -> + SongsMenuHelper.INSTANCE.handleMenuClick(requireActivity(), songs, itemId)) + .execute(new ListSongsAsyncTask.LoadingInfo(files, AUDIO_FILE_FILTER, getFileComparator())); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + ToolbarContentTintHelper.handleOnPrepareOptionsMenu(requireActivity(), toolbar); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.add(0, R.id.action_scan, 0, R.string.scan_media) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.add(0, R.id.action_go_to_start_directory, 1, R.string.action_go_to_start_directory) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + menu.removeItem(R.id.action_grid_size); + menu.removeItem(R.id.action_layout_type); + menu.removeItem(R.id.action_sort_order); + ToolbarContentTintHelper.handleOnCreateOptionsMenu( + requireContext(), toolbar, menu, getToolbarBackgroundColor(toolbar)); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_go_to_start_directory: + setCrumb( + new BreadCrumbLayout.Crumb( + tryGetCanonicalFile(PreferenceUtil.INSTANCE.getStartDirectory())), + true); + return true; + case R.id.action_scan: + BreadCrumbLayout.Crumb crumb = getActiveCrumb(); + if (crumb != null) { + //noinspection Convert2MethodRef + new ListPathsAsyncTask(getActivity(), paths -> scanPaths(paths)) + .execute(new ListPathsAsyncTask.LoadingInfo(crumb.getFile(), AUDIO_FILE_FILTER)); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onQueueChanged() { + super.onQueueChanged(); + checkForPadding(); + } + + @Override + public void onServiceConnected() { + super.onServiceConnected(); + checkForPadding(); + } + + @NonNull + @Override + public MaterialCab openCab(int menuRes, @NotNull MaterialCab.Callback callback) { + if (cab != null && cab.isActive()) { + cab.finish(); + } + cab = + new MaterialCab(getMainActivity(), R.id.cab_stub) + .setMenu(menuRes) + .setCloseDrawableRes(R.drawable.ic_close) + .setBackgroundColor( + RetroColorUtil.shiftBackgroundColorForLightText( + ATHUtil.INSTANCE.resolveColor(requireContext(), R.attr.colorSurface))) + .start(callback); + return cab; + } + + private void checkForPadding() { + final int count = adapter.getItemCount(); + final MarginLayoutParams params = (MarginLayoutParams) coordinatorLayout.getLayoutParams(); + params.bottomMargin = + count > 0 && !MusicPlayerRemote.getPlayingQueue().isEmpty() + ? DensityUtil.dip2px(requireContext(), 104f) + : DensityUtil.dip2px(requireContext(), 54f); + } + + private void checkIsEmpty() { + emojiText.setText(getEmojiByUnicode(0x1F631)); + if (empty != null) { + empty.setVisibility( + adapter == null || adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + } + + @Nullable + private BreadCrumbLayout.Crumb getActiveCrumb() { + return breadCrumbs != null && breadCrumbs.size() > 0 + ? breadCrumbs.getCrumb(breadCrumbs.getActiveIndex()) + : null; + } + + private String getEmojiByUnicode(int unicode) { + return new String(Character.toChars(unicode)); + } + + private Comparator getFileComparator() { + return fileComparator; + } + + private void initViews(View view) { + coordinatorLayout = view.findViewById(R.id.coordinatorLayout); + recyclerView = view.findViewById(R.id.recyclerView); + breadCrumbs = view.findViewById(R.id.breadCrumbs); + empty = view.findViewById(android.R.id.empty); + emojiText = view.findViewById(R.id.emptyEmoji); + toolbar = view.findViewById(R.id.toolbar); + appNameText = view.findViewById(R.id.appNameText); + } + + private void saveScrollPosition() { BreadCrumbLayout.Crumb crumb = getActiveCrumb(); if (crumb != null) { - //noinspection Convert2MethodRef - new ListPathsAsyncTask(getActivity(), paths -> scanPaths(paths)) - .execute(new ListPathsAsyncTask.LoadingInfo(crumb.getFile(), AUDIO_FILE_FILTER)); + crumb.setScrollPosition( + ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition()); } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onQueueChanged() { - super.onQueueChanged(); - checkForPadding(); - } - - @Override - public void onServiceConnected() { - super.onServiceConnected(); - checkForPadding(); - } - - @NonNull - @Override - public MaterialCab openCab(int menuRes, @NotNull MaterialCab.Callback callback) { - if (cab != null && cab.isActive()) { - cab.finish(); - } - cab = - new MaterialCab(getMainActivity(), R.id.cab_stub) - .setMenu(menuRes) - .setCloseDrawableRes(R.drawable.ic_close) - .setBackgroundColor( - RetroColorUtil.shiftBackgroundColorForLightText( - ATHUtil.INSTANCE.resolveColor(requireContext(), R.attr.colorSurface))) - .start(callback); - return cab; - } - - private void checkForPadding() { - final int count = adapter.getItemCount(); - final MarginLayoutParams params = (MarginLayoutParams) coordinatorLayout.getLayoutParams(); - params.bottomMargin = - count > 0 && !MusicPlayerRemote.getPlayingQueue().isEmpty() - ? DensityUtil.dip2px(requireContext(), 104f) - : DensityUtil.dip2px(requireContext(), 54f); - } - - private void checkIsEmpty() { - emojiText.setText(getEmojiByUnicode(0x1F631)); - if (empty != null) { - empty.setVisibility( - adapter == null || adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - } - - @Nullable - private BreadCrumbLayout.Crumb getActiveCrumb() { - return breadCrumbs != null && breadCrumbs.size() > 0 - ? breadCrumbs.getCrumb(breadCrumbs.getActiveIndex()) - : null; - } - - private String getEmojiByUnicode(int unicode) { - return new String(Character.toChars(unicode)); - } - - private Comparator getFileComparator() { - return fileComparator; - } - - private void initViews(View view) { - coordinatorLayout = view.findViewById(R.id.coordinatorLayout); - recyclerView = view.findViewById(R.id.recyclerView); - breadCrumbs = view.findViewById(R.id.breadCrumbs); - empty = view.findViewById(android.R.id.empty); - emojiText = view.findViewById(R.id.emptyEmoji); - toolbar = view.findViewById(R.id.toolbar); - appNameText = view.findViewById(R.id.appNameText); - } - - private void saveScrollPosition() { - BreadCrumbLayout.Crumb crumb = getActiveCrumb(); - if (crumb != null) { - crumb.setScrollPosition( - ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition()); - } - } - - private void scanPaths(@Nullable String[] toBeScanned) { - if (getActivity() == null) { - return; - } - if (toBeScanned == null || toBeScanned.length < 1) { - Toast.makeText(getActivity(), R.string.nothing_to_scan, Toast.LENGTH_SHORT).show(); - } else { - MediaScannerConnection.scanFile( - getActivity().getApplicationContext(), - toBeScanned, - null, - new UpdateToastMediaScannerCompletionListener(getActivity(), toBeScanned)); - } - } - - private void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) { - if (crumb == null) { - return; - } - saveScrollPosition(); - breadCrumbs.setActiveOrAdd(crumb, false); - if (addToHistory) { - breadCrumbs.addHistory(crumb); - } - LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this); - } - - private void setUpAdapter() { - adapter = - new SongFileAdapter(getMainActivity(), new LinkedList<>(), R.layout.item_list, this, this); - adapter.registerAdapterDataObserver( - new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - super.onChanged(); - checkIsEmpty(); - checkForPadding(); - } - }); - recyclerView.setAdapter(adapter); - checkIsEmpty(); - } - - private void setUpAppbarColor() { - breadCrumbs.setActivatedContentColor( - ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorPrimary)); - breadCrumbs.setDeactivatedContentColor( - ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorSecondary)); - } - - private void setUpBreadCrumbs() { - breadCrumbs.setCallback(this); - } - - private void setUpRecyclerView() { - recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - FastScroller fastScroller = ThemedFastScroller.INSTANCE.create(recyclerView); - recyclerView.setOnApplyWindowInsetsListener( - new ScrollingViewOnApplyWindowInsetsListener(recyclerView, fastScroller)); - } - - private ArrayList toList(File file) { - ArrayList files = new ArrayList<>(1); - files.add(file); - return files; - } - - private void updateAdapter(@NonNull List files) { - adapter.swapDataSet(files); - BreadCrumbLayout.Crumb crumb = getActiveCrumb(); - if (crumb != null && recyclerView != null) { - ((LinearLayoutManager) recyclerView.getLayoutManager()) - .scrollToPositionWithOffset(crumb.getScrollPosition(), 0); - } - } - - public static class ListPathsAsyncTask - extends ListingFilesDialogAsyncTask { - - private WeakReference onPathsListedCallbackWeakReference; - - public ListPathsAsyncTask(Context context, OnPathsListedCallback callback) { - super(context); - onPathsListedCallbackWeakReference = new WeakReference<>(callback); } - @Override - protected String[] doInBackground(LoadingInfo... params) { - try { - if (isCancelled() || checkCallbackReference() == null) { - return null; + private void scanPaths(@Nullable String[] toBeScanned) { + if (getActivity() == null) { + return; } - - LoadingInfo info = params[0]; - - final String[] paths; - - if (info.file.isDirectory()) { - List files = FileUtil.listFilesDeep(info.file, info.fileFilter); - - if (isCancelled() || checkCallbackReference() == null) { - return null; - } - - paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) { - File f = files.get(i); - paths[i] = FileUtil.safeGetCanonicalPath(f); - - if (isCancelled() || checkCallbackReference() == null) { - return null; - } - } + if (toBeScanned == null || toBeScanned.length < 1) { + Toast.makeText(getActivity(), R.string.nothing_to_scan, Toast.LENGTH_SHORT).show(); } else { - paths = new String[1]; - paths[0] = info.file.getPath(); + MediaScannerConnection.scanFile( + getActivity().getApplicationContext(), + toBeScanned, + null, + new UpdateToastMediaScannerCompletionListener(getActivity(), toBeScanned)); } - - return paths; - } catch (Exception e) { - e.printStackTrace(); - cancel(false); - return null; - } } - @Override - protected void onPostExecute(String[] paths) { - super.onPostExecute(paths); - OnPathsListedCallback callback = checkCallbackReference(); - if (callback != null && paths != null) { - callback.onPathsListed(paths); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - checkCallbackReference(); - } - - private OnPathsListedCallback checkCallbackReference() { - OnPathsListedCallback callback = onPathsListedCallbackWeakReference.get(); - if (callback == null) { - cancel(false); - } - return callback; - } - - public interface OnPathsListedCallback { - - void onPathsListed(@NonNull String[] paths); - } - - public static class LoadingInfo { - - public final File file; - - final FileFilter fileFilter; - - public LoadingInfo(File file, FileFilter fileFilter) { - this.file = file; - this.fileFilter = fileFilter; - } - } - } - - private static class AsyncFileLoader extends WrappedAsyncTaskLoader> { - - private WeakReference fragmentWeakReference; - - AsyncFileLoader(FoldersFragment foldersFragment) { - super(foldersFragment.requireActivity()); - fragmentWeakReference = new WeakReference<>(foldersFragment); - } - - @Override - public List loadInBackground() { - FoldersFragment foldersFragment = fragmentWeakReference.get(); - File directory = null; - if (foldersFragment != null) { - BreadCrumbLayout.Crumb crumb = foldersFragment.getActiveCrumb(); - if (crumb != null) { - directory = crumb.getFile(); + private void setCrumb(BreadCrumbLayout.Crumb crumb, boolean addToHistory) { + if (crumb == null) { + return; } - } - if (directory != null) { - List files = FileUtil.listFiles(directory, AUDIO_FILE_FILTER); - Collections.sort(files, foldersFragment.getFileComparator()); + saveScrollPosition(); + breadCrumbs.setActiveOrAdd(crumb, false); + if (addToHistory) { + breadCrumbs.addHistory(crumb); + } + LoaderManager.getInstance(this).restartLoader(LOADER_ID, null, this); + } + + private void setUpAdapter() { + adapter = + new SongFileAdapter(getMainActivity(), new LinkedList<>(), R.layout.item_list, this, this); + adapter.registerAdapterDataObserver( + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + checkIsEmpty(); + checkForPadding(); + } + }); + recyclerView.setAdapter(adapter); + checkIsEmpty(); + } + + private void setUpAppbarColor() { + breadCrumbs.setActivatedContentColor( + ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorPrimary)); + breadCrumbs.setDeactivatedContentColor( + ATHUtil.INSTANCE.resolveColor(requireContext(), android.R.attr.textColorSecondary)); + } + + private void setUpBreadCrumbs() { + breadCrumbs.setCallback(this); + } + + private void setUpRecyclerView() { + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + FastScroller fastScroller = ThemedFastScroller.INSTANCE.create(recyclerView); + recyclerView.setOnApplyWindowInsetsListener( + new ScrollingViewOnApplyWindowInsetsListener(recyclerView, fastScroller)); + } + + private ArrayList toList(File file) { + ArrayList files = new ArrayList<>(1); + files.add(file); return files; - } else { - return new LinkedList<>(); - } - } - } - - private static class ListSongsAsyncTask - extends ListingFilesDialogAsyncTask> { - - private final Object extra; - private WeakReference callbackWeakReference; - private WeakReference contextWeakReference; - - ListSongsAsyncTask(Context context, Object extra, OnSongsListedCallback callback) { - super(context); - this.extra = extra; - contextWeakReference = new WeakReference<>(context); - callbackWeakReference = new WeakReference<>(callback); } - @Override - protected List doInBackground(LoadingInfo... params) { - try { - LoadingInfo info = params[0]; - List files = FileUtil.listFilesDeep(info.files, info.fileFilter); + private void updateAdapter(@NonNull List files) { + adapter.swapDataSet(files); + BreadCrumbLayout.Crumb crumb = getActiveCrumb(); + if (crumb != null && recyclerView != null) { + ((LinearLayoutManager) recyclerView.getLayoutManager()) + .scrollToPositionWithOffset(crumb.getScrollPosition(), 0); + } + } - if (isCancelled() || checkContextReference() == null || checkCallbackReference() == null) { - return null; + public static class ListPathsAsyncTask + extends ListingFilesDialogAsyncTask { + + private WeakReference onPathsListedCallbackWeakReference; + + public ListPathsAsyncTask(Context context, OnPathsListedCallback callback) { + super(context); + onPathsListedCallbackWeakReference = new WeakReference<>(callback); } - Collections.sort(files, info.fileComparator); + @Override + protected String[] doInBackground(LoadingInfo... params) { + try { + if (isCancelled() || checkCallbackReference() == null) { + return null; + } - Context context = checkContextReference(); - if (isCancelled() || context == null || checkCallbackReference() == null) { - return null; + LoadingInfo info = params[0]; + + final String[] paths; + + if (info.file.isDirectory()) { + List files = FileUtil.listFilesDeep(info.file, info.fileFilter); + + if (isCancelled() || checkCallbackReference() == null) { + return null; + } + + paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) { + File f = files.get(i); + paths[i] = FileUtil.safeGetCanonicalPath(f); + + if (isCancelled() || checkCallbackReference() == null) { + return null; + } + } + } else { + paths = new String[1]; + paths[0] = info.file.getPath(); + } + + return paths; + } catch (Exception e) { + e.printStackTrace(); + cancel(false); + return null; + } } - return FileUtil.matchFilesWithMediaStore(context, files); - } catch (Exception e) { - e.printStackTrace(); - cancel(false); - return null; - } + @Override + protected void onPostExecute(String[] paths) { + super.onPostExecute(paths); + OnPathsListedCallback callback = checkCallbackReference(); + if (callback != null && paths != null) { + callback.onPathsListed(paths); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + checkCallbackReference(); + } + + private OnPathsListedCallback checkCallbackReference() { + OnPathsListedCallback callback = onPathsListedCallbackWeakReference.get(); + if (callback == null) { + cancel(false); + } + return callback; + } + + public interface OnPathsListedCallback { + + void onPathsListed(@NonNull String[] paths); + } + + public static class LoadingInfo { + + public final File file; + + final FileFilter fileFilter; + + public LoadingInfo(File file, FileFilter fileFilter) { + this.file = file; + this.fileFilter = fileFilter; + } + } } - @Override - protected void onPostExecute(List songs) { - super.onPostExecute(songs); - OnSongsListedCallback callback = checkCallbackReference(); - if (songs != null && callback != null) { - callback.onSongsListed(songs, extra); - } + private static class AsyncFileLoader extends WrappedAsyncTaskLoader> { + + private WeakReference fragmentWeakReference; + + AsyncFileLoader(FoldersFragment foldersFragment) { + super(foldersFragment.requireActivity()); + fragmentWeakReference = new WeakReference<>(foldersFragment); + } + + @Override + public List loadInBackground() { + FoldersFragment foldersFragment = fragmentWeakReference.get(); + File directory = null; + if (foldersFragment != null) { + BreadCrumbLayout.Crumb crumb = foldersFragment.getActiveCrumb(); + if (crumb != null) { + directory = crumb.getFile(); + } + } + if (directory != null) { + List files = FileUtil.listFiles(directory, AUDIO_FILE_FILTER); + Collections.sort(files, foldersFragment.getFileComparator()); + return files; + } else { + return new LinkedList<>(); + } + } } - @Override - protected void onPreExecute() { - super.onPreExecute(); - checkCallbackReference(); - checkContextReference(); + private static class ListSongsAsyncTask + extends ListingFilesDialogAsyncTask> { + + private final Object extra; + private WeakReference callbackWeakReference; + private WeakReference contextWeakReference; + + ListSongsAsyncTask(Context context, Object extra, OnSongsListedCallback callback) { + super(context); + this.extra = extra; + contextWeakReference = new WeakReference<>(context); + callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected List doInBackground(LoadingInfo... params) { + try { + LoadingInfo info = params[0]; + List files = FileUtil.listFilesDeep(info.files, info.fileFilter); + + if (isCancelled() || checkContextReference() == null || checkCallbackReference() == null) { + return null; + } + + Collections.sort(files, info.fileComparator); + + Context context = checkContextReference(); + if (isCancelled() || context == null || checkCallbackReference() == null) { + return null; + } + + return FileUtil.matchFilesWithMediaStore(context, files); + } catch (Exception e) { + e.printStackTrace(); + cancel(false); + return null; + } + } + + @Override + protected void onPostExecute(List songs) { + super.onPostExecute(songs); + OnSongsListedCallback callback = checkCallbackReference(); + if (songs != null && callback != null) { + callback.onSongsListed(songs, extra); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + checkCallbackReference(); + checkContextReference(); + } + + private OnSongsListedCallback checkCallbackReference() { + OnSongsListedCallback callback = callbackWeakReference.get(); + if (callback == null) { + cancel(false); + } + return callback; + } + + private Context checkContextReference() { + Context context = contextWeakReference.get(); + if (context == null) { + cancel(false); + } + return context; + } + + public interface OnSongsListedCallback { + + void onSongsListed(@NonNull List songs, Object extra); + } + + static class LoadingInfo { + + final Comparator fileComparator; + + final FileFilter fileFilter; + + final List files; + + LoadingInfo( + @NonNull List files, + @NonNull FileFilter fileFilter, + @NonNull Comparator fileComparator) { + this.fileComparator = fileComparator; + this.fileFilter = fileFilter; + this.files = files; + } + } } - private OnSongsListedCallback checkCallbackReference() { - OnSongsListedCallback callback = callbackWeakReference.get(); - if (callback == null) { - cancel(false); - } - return callback; + private abstract static class ListingFilesDialogAsyncTask + extends DialogAsyncTask { + + ListingFilesDialogAsyncTask(Context context) { + super(context); + } + + public ListingFilesDialogAsyncTask(Context context, int showDelay) { + super(context, showDelay); + } + + @Override + protected Dialog createDialog(@NonNull Context context) { + return new MaterialAlertDialogBuilder(context) + .setTitle(R.string.listing_files) + .setCancelable(false) + .setView(R.layout.loading) + .setOnCancelListener(dialog -> cancel(false)) + .setOnDismissListener(dialog -> cancel(false)) + .create(); + } } - - private Context checkContextReference() { - Context context = contextWeakReference.get(); - if (context == null) { - cancel(false); - } - return context; - } - - public interface OnSongsListedCallback { - - void onSongsListed(@NonNull List songs, Object extra); - } - - static class LoadingInfo { - - final Comparator fileComparator; - - final FileFilter fileFilter; - - final List files; - - LoadingInfo( - @NonNull List files, - @NonNull FileFilter fileFilter, - @NonNull Comparator fileComparator) { - this.fileComparator = fileComparator; - this.fileFilter = fileFilter; - this.files = files; - } - } - } - - private abstract static class ListingFilesDialogAsyncTask - extends DialogAsyncTask { - - ListingFilesDialogAsyncTask(Context context) { - super(context); - } - - public ListingFilesDialogAsyncTask(Context context, int showDelay) { - super(context, showDelay); - } - - @Override - protected Dialog createDialog(@NonNull Context context) { - return new MaterialAlertDialogBuilder(context) - .setTitle(R.string.listing_files) - .setCancelable(false) - .setView(R.layout.loading) - .setOnCancelListener(dialog -> cancel(false)) - .setOnDismissListener(dialog -> cancel(false)) - .create(); - } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index 3868665c4..520d0836a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -52,6 +52,7 @@ class HomeFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + println("AbsMainActivityFragment") libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt index dc3e874c8..35d4f2926 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt @@ -5,10 +5,12 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.song.PlaylistSongAdapter import code.name.monkey.retromusic.db.PlaylistWithSongs @@ -18,6 +20,7 @@ import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.state.NowPlayingPanelState +import com.google.android.material.transition.MaterialContainerTransform import kotlinx.android.synthetic.main.fragment_playlist_detail.* import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -31,18 +34,29 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli private lateinit var playlist: PlaylistWithSongs private lateinit var playlistSongAdapter: PlaylistSongAdapter - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + private fun setUpTransitions() { + val transform = MaterialContainerTransform() + transform.setAllContainerColors(ATHUtil.resolveColor(requireContext(), R.attr.colorSurface)) + sharedElementEnterTransition = transform + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpTransitions() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) mainActivity.addMusicServiceEventListener(viewModel) mainActivity.setSupportActionBar(toolbar) + ViewCompat.setTransitionName(container, "playlist") playlist = arguments.extraPlaylist toolbar.title = playlist.playlistEntity.playlistName setUpRecyclerView() - viewModel.getSongs().observe(viewLifecycleOwner, { songs(it.toSongs()) }) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt index bb97538d9..3f6f874e6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsViewModel.kt @@ -33,26 +33,7 @@ class PlaylistDetailsViewModel( fun getSongs(): LiveData> = realRepository.playlistSongs(playlist.playlistEntity.playListId) - override fun onMediaStoreChanged() { - /*if (playlist !is AbsCustomPlaylist) { - // Playlist deleted - if (!PlaylistsUtil.doesPlaylistExist(App.getContext(), playlist.id)) { - //TODO Finish the page - return - } - // Playlist renamed - val playlistName = - PlaylistsUtil.getNameForPlaylist(App.getContext(), playlist.id.toLong()) - if (playlistName != playlist.name) { - viewModelScope.launch { - playlist = realRepository.playlist(playlist.id) - _playlist.postValue(playlist) - } - } - } - loadPlaylistSongs(playlist)*/ - } - + override fun onMediaStoreChanged() {} override fun onServiceConnected() {} override fun onServiceDisconnected() {} override fun onQueueChanged() {} diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt index 275929357..cc5ef0fc2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt @@ -18,19 +18,31 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.SubMenu import android.view.View -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.core.os.bundleOf +import androidx.core.view.MenuCompat +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.GridLayoutManager import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper +import code.name.monkey.retromusic.EXTRA_PLAYLIST import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.playlist.PlaylistAdapter -import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewFragment +import code.name.monkey.retromusic.db.PlaylistWithSongs +import code.name.monkey.retromusic.fragments.ReloadType +import code.name.monkey.retromusic.fragments.base.AbsRecyclerViewCustomGridSizeFragment +import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder +import code.name.monkey.retromusic.interfaces.IPlaylistClickListener +import code.name.monkey.retromusic.util.PreferenceUtil import kotlinx.android.synthetic.main.fragment_library.* -class PlaylistsFragment : AbsRecyclerViewFragment() { +class PlaylistsFragment : + AbsRecyclerViewCustomGridSizeFragment(), + IPlaylistClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - libraryViewModel.getPlaylists().observe(viewLifecycleOwner, Observer { + libraryViewModel.getPlaylists().observe(viewLifecycleOwner, { if (it.isNotEmpty()) adapter?.swapDataSet(it) else @@ -41,8 +53,8 @@ class PlaylistsFragment : AbsRecyclerViewFragment PlaylistSortOrder.PLAYLIST_A_Z + R.id.action_song_sort_order_desc -> PlaylistSortOrder.PLAYLIST_Z_A + R.id.action_playlist_sort_order -> PlaylistSortOrder.PLAYLIST_SONG_COUNT + R.id.action_playlist_sort_order_desc -> PlaylistSortOrder.PLAYLIST_SONG_COUNT_DESC + else -> PreferenceUtil.playlistSortOrder + } + if (sortOrder != PreferenceUtil.playlistSortOrder) { + item.isChecked = true + setAndSaveSortOrder(sortOrder) + return true + } + return false + } + + private fun createId(menu: SubMenu, id: Int, title: Int, checked: Boolean) { + menu.add(0, id, 0, title).isChecked = checked + } + + + override fun setGridSize(gridSize: Int) { + TODO("Not yet implemented") + } + + override fun setSortOrder(sortOrder: String) { + libraryViewModel.forceReload(ReloadType.Playlists) + } + + override fun loadSortOrder(): String { + return PreferenceUtil.playlistSortOrder + } + + override fun saveSortOrder(sortOrder: String) { + PreferenceUtil.playlistSortOrder = sortOrder + } + + override fun loadGridSize(): Int { + return 1 + } + + override fun saveGridSize(gridColumns: Int) { + //Add grid save + } + + override fun loadGridSizeLand(): Int { + return 2 + } + + override fun saveGridSizeLand(gridColumns: Int) { + //Add land grid save + } + + override fun loadLayoutRes(): Int { + return R.layout.item_list + } + + override fun saveLayoutRes(layoutRes: Int) { + //Save layout + } + + override fun onPlaylistClick(playlistWithSongs: PlaylistWithSongs, view: View) { + findNavController().navigate( + R.id.playlistDetailsFragment, + bundleOf(EXTRA_PLAYLIST to playlistWithSongs), + null, + FragmentNavigatorExtras(view to "playlist") + ) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt b/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt index b02ded2ac..22f92a09f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/SortOrder.kt @@ -184,4 +184,25 @@ class SortOrder { const val ALBUM_Z_A = "$GENRE_A_Z DESC" } } + + /** + * Playlist sort order entries. + */ + interface PlaylistSortOrder { + + companion object { + + /* Playlist sort order A-Z */ + const val PLAYLIST_A_Z = MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER + + /* Playlist sort order Z-A */ + const val PLAYLIST_Z_A = "$PLAYLIST_A_Z DESC" + + /* Playlist sort order number of songs */ + const val PLAYLIST_SONG_COUNT = "playlist_song_count" + + /* Playlist sort order number of songs */ + const val PLAYLIST_SONG_COUNT_DESC = "$PLAYLIST_SONG_COUNT DESC" + } + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/interfaces/IPlaylistClickListener.kt b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPlaylistClickListener.kt new file mode 100644 index 000000000..3b0eb141d --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/interfaces/IPlaylistClickListener.kt @@ -0,0 +1,8 @@ +package code.name.monkey.retromusic.interfaces + +import android.view.View +import code.name.monkey.retromusic.db.PlaylistWithSongs + +interface IPlaylistClickListener { + fun onPlaylistClick(playlistWithSongs: PlaylistWithSongs, view: View) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt index 948190438..ca7c822a7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/GenreRepository.kt @@ -95,13 +95,17 @@ class RealGenreRepository( } private fun makeGenreSongCursor(genreId: Long): Cursor? { - return contentResolver.query( - Genres.Members.getContentUri("external", genreId), - baseProjection, - IS_MUSIC, - null, - PreferenceUtil.songSortOrder - ) + return try { + contentResolver.query( + Genres.Members.getContentUri("external", genreId), + baseProjection, + IS_MUSIC, + null, + PreferenceUtil.songSortOrder + ) + } catch (e: SecurityException) { + return null + } } private fun getGenresFromCursor(cursor: Cursor?): ArrayList { @@ -143,17 +147,18 @@ class RealGenreRepository( return genres } - private fun makeGenreCursor(): Cursor? { val projection = arrayOf(Genres._ID, Genres.NAME) - return contentResolver.query( - Genres.EXTERNAL_CONTENT_URI, - projection, - null, - null, - PreferenceUtil.genreSortOrder - ) + return try { + contentResolver.query( + Genres.EXTERNAL_CONTENT_URI, + projection, + null, + null, + PreferenceUtil.genreSortOrder + ) + } catch (e: SecurityException) { + return null + } } - - } diff --git a/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt b/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt index ca8a9833d..e57c5c57c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/repository/RoomRepository.kt @@ -2,8 +2,24 @@ package code.name.monkey.retromusic.repository import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData -import code.name.monkey.retromusic.db.* +import code.name.monkey.retromusic.db.BlackListStoreDao +import code.name.monkey.retromusic.db.BlackListStoreEntity +import code.name.monkey.retromusic.db.HistoryDao +import code.name.monkey.retromusic.db.HistoryEntity +import code.name.monkey.retromusic.db.LyricsDao +import code.name.monkey.retromusic.db.PlayCountDao +import code.name.monkey.retromusic.db.PlayCountEntity +import code.name.monkey.retromusic.db.PlaylistDao +import code.name.monkey.retromusic.db.PlaylistEntity +import code.name.monkey.retromusic.db.PlaylistWithSongs +import code.name.monkey.retromusic.db.SongEntity +import code.name.monkey.retromusic.db.toHistoryEntity +import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder.Companion.PLAYLIST_A_Z +import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder.Companion.PLAYLIST_SONG_COUNT +import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder.Companion.PLAYLIST_SONG_COUNT_DESC +import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder.Companion.PLAYLIST_Z_A import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.util.PreferenceUtil interface RoomRepository { @@ -61,7 +77,22 @@ class RealRoomRepository( @WorkerThread override suspend fun playlistWithSongs(): List = - playlistDao.playlistsWithSongs() + when (PreferenceUtil.playlistSortOrder) { + PLAYLIST_A_Z -> + playlistDao.playlistsWithSongs().sortedBy { + it.playlistEntity.playlistName + } + PLAYLIST_Z_A -> playlistDao.playlistsWithSongs() + .sortedByDescending { + it.playlistEntity.playlistName + } + PLAYLIST_SONG_COUNT -> playlistDao.playlistsWithSongs().sortedBy { it.songs.size } + PLAYLIST_SONG_COUNT_DESC -> playlistDao.playlistsWithSongs() + .sortedByDescending { it.songs.size } + else -> playlistDao.playlistsWithSongs().sortedBy { + it.playlistEntity.playlistName + } + } @WorkerThread override suspend fun insertSongs(songs: List) { diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index f9abe7d60..b7d399720 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -14,13 +14,6 @@ package code.name.monkey.retromusic.service; -import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN; -import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART; -import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION; -import static code.name.monkey.retromusic.ConstantsKt.GAP_LESS_PLAYBACK; -import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; - import android.app.PendingIntent; import android.app.Service; import android.appwidget.AppWidgetManager; @@ -54,9 +47,21 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; + +import com.bumptech.glide.BitmapRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.SimpleTarget; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; + import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.LockScreenActivity; import code.name.monkey.retromusic.appwidgets.AppWidgetBig; @@ -79,1368 +84,1359 @@ import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.MusicUtil; import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroUtil; -import com.bumptech.glide.BitmapRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.SimpleTarget; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Random; -/** @author Karim Abou Zeid (kabouzeid), Andrew Neal */ +import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN; +import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART; +import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION; +import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION; +import static code.name.monkey.retromusic.ConstantsKt.GAP_LESS_PLAYBACK; +import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; + +/** + * @author Karim Abou Zeid (kabouzeid), Andrew Neal + */ public class MusicService extends Service - implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { + implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { - public static final String TAG = MusicService.class.getSimpleName(); - public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; - public static final String MUSIC_PACKAGE_NAME = "com.android.music"; - public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; - public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; - public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; - public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; - public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; - public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; - public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; - public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; - public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; - public static final String INTENT_EXTRA_PLAYLIST = - RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; - public static final String INTENT_EXTRA_SHUFFLE_MODE = - RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; - public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; - public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; - // Do not change these three strings as it will break support with other apps (e.g. last.fm - // scrobbling) - public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; - public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; - public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; - public static final String FAVORITE_STATE_CHANGED = - RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; - public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; - public static final String SHUFFLE_MODE_CHANGED = - RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; - public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; - public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; - public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; - public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; - public static final String SAVED_POSITION = "POSITION"; - public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; - public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; - public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; - public static final int RELEASE_WAKELOCK = 0; - public static final int TRACK_ENDED = 1; - public static final int TRACK_WENT_TO_NEXT = 2; - public static final int PLAY_SONG = 3; - public static final int PREPARE_NEXT = 4; - public static final int SET_POSITION = 5; - public static final int FOCUS_CHANGE = 6; - public static final int DUCK = 7; - public static final int UNDUCK = 8; - public static final int RESTORE_QUEUES = 9; - public static final int SHUFFLE_MODE_NONE = 0; - public static final int SHUFFLE_MODE_SHUFFLE = 1; - public static final int REPEAT_MODE_NONE = 0; - public static final int REPEAT_MODE_ALL = 1; - public static final int REPEAT_MODE_THIS = 2; - public static final int SAVE_QUEUES = 0; - private static final long MEDIA_SESSION_ACTIONS = - PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_STOP - | PlaybackStateCompat.ACTION_SEEK_TO; - private final IBinder musicBind = new MusicBinder(); - public int nextPosition = -1; + public static final String TAG = MusicService.class.getSimpleName(); + public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; + public static final String MUSIC_PACKAGE_NAME = "com.android.music"; + public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; + public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; + public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; + public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; + public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; + public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; + public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; + public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; + public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; + public static final String INTENT_EXTRA_PLAYLIST = + RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; + public static final String INTENT_EXTRA_SHUFFLE_MODE = + RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; + public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; + public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; + // Do not change these three strings as it will break support with other apps (e.g. last.fm + // scrobbling) + public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; + public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; + public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; + public static final String FAVORITE_STATE_CHANGED = + RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; + public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; + public static final String SHUFFLE_MODE_CHANGED = + RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; + public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; + public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; + public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; + public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; + public static final String SAVED_POSITION = "POSITION"; + public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; + public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; + public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; + public static final int RELEASE_WAKELOCK = 0; + public static final int TRACK_ENDED = 1; + public static final int TRACK_WENT_TO_NEXT = 2; + public static final int PLAY_SONG = 3; + public static final int PREPARE_NEXT = 4; + public static final int SET_POSITION = 5; + public static final int FOCUS_CHANGE = 6; + public static final int DUCK = 7; + public static final int UNDUCK = 8; + public static final int RESTORE_QUEUES = 9; + public static final int SHUFFLE_MODE_NONE = 0; + public static final int SHUFFLE_MODE_SHUFFLE = 1; + public static final int REPEAT_MODE_NONE = 0; + public static final int REPEAT_MODE_ALL = 1; + public static final int REPEAT_MODE_THIS = 2; + public static final int SAVE_QUEUES = 0; + private static final long MEDIA_SESSION_ACTIONS = + PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_STOP + | PlaybackStateCompat.ACTION_SEEK_TO; + private final IBinder musicBind = new MusicBinder(); + public int nextPosition = -1; - public boolean pendingQuit = false; + public boolean pendingQuit = false; - @Nullable public Playback playback; + @Nullable + public Playback playback; - public int position = -1; + public int position = -1; - private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); + private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); - private AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance(); + private AppWidgetCard appWidgetCard = AppWidgetCard.Companion.getInstance(); - private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); + private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); - private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); + private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); - private AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance(); + private AppWidgetText appWidgetText = AppWidgetText.Companion.getInstance(); - private final BroadcastReceiver widgetIntentReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String command = intent.getStringExtra(EXTRA_APP_WIDGET_NAME); - final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); - if (command != null) { - switch (command) { - case AppWidgetClassic.NAME: - { - appWidgetClassic.performUpdate(MusicService.this, ids); - break; + private final BroadcastReceiver widgetIntentReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final String command = intent.getStringExtra(EXTRA_APP_WIDGET_NAME); + final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (command != null) { + switch (command) { + case AppWidgetClassic.NAME: { + appWidgetClassic.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetSmall.NAME: { + appWidgetSmall.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetBig.NAME: { + appWidgetBig.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetCard.NAME: { + appWidgetCard.performUpdate(MusicService.this, ids); + break; + } + case AppWidgetText.NAME: { + appWidgetText.performUpdate(MusicService.this, ids); + break; + } + } + } } - case AppWidgetSmall.NAME: - { - appWidgetSmall.performUpdate(MusicService.this, ids); - break; + }; + private AudioManager audioManager; + private IntentFilter becomingNoisyReceiverIntentFilter = + new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + private boolean becomingNoisyReceiverRegistered; + private IntentFilter bluetoothConnectedIntentFilter = + new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); + private boolean bluetoothConnectedRegistered = false; + private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + private boolean headsetReceiverRegistered = false; + private MediaSessionCompat mediaSession; + private ContentObserver mediaStoreObserver; + private HandlerThread musicPlayerHandlerThread; + private boolean notHandledMetaChangedForCurrentTrack; + private List originalPlayingQueue = new ArrayList<>(); + private List playingQueue = new ArrayList<>(); + private boolean pausedByTransientLossOfFocus; + + private final BroadcastReceiver becomingNoisyReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, @NonNull Intent intent) { + if (intent.getAction() != null + && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { + pause(); + } } - case AppWidgetBig.NAME: - { - appWidgetBig.performUpdate(MusicService.this, ids); - break; + }; + + private PlaybackHandler playerHandler; + + private final AudioManager.OnAudioFocusChangeListener audioFocusListener = + new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(final int focusChange) { + playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); } - case AppWidgetCard.NAME: - { - appWidgetCard.performUpdate(MusicService.this, ids); - break; + }; + + private PlayingNotification playingNotification; + private final BroadcastReceiver updateFavoriteReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + updateNotification(); } - case AppWidgetText.NAME: - { - appWidgetText.performUpdate(MusicService.this, ids); - break; + }; + private final BroadcastReceiver lockScreenReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) { + Intent lockIntent = new Intent(context, LockScreenActivity.class); + lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(lockIntent); + } } - } - } - } - }; - private AudioManager audioManager; - private IntentFilter becomingNoisyReceiverIntentFilter = - new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - private boolean becomingNoisyReceiverRegistered; - private IntentFilter bluetoothConnectedIntentFilter = - new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED); - private boolean bluetoothConnectedRegistered = false; - private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - private boolean headsetReceiverRegistered = false; - private MediaSessionCompat mediaSession; - private ContentObserver mediaStoreObserver; - private HandlerThread musicPlayerHandlerThread; - private boolean notHandledMetaChangedForCurrentTrack; - private List originalPlayingQueue = new ArrayList<>(); - private List playingQueue = new ArrayList<>(); - private boolean pausedByTransientLossOfFocus; - - private final BroadcastReceiver becomingNoisyReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, @NonNull Intent intent) { - if (intent.getAction() != null - && intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { - pause(); - } - } - }; - - private PlaybackHandler playerHandler; - - private final AudioManager.OnAudioFocusChangeListener audioFocusListener = - new AudioManager.OnAudioFocusChangeListener() { - @Override - public void onAudioFocusChange(final int focusChange) { - playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); - } - }; - - private PlayingNotification playingNotification; - private final BroadcastReceiver updateFavoriteReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - updateNotification(); - } - }; - private final BroadcastReceiver lockScreenReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (PreferenceUtil.INSTANCE.isLockScreen() && isPlaying()) { - Intent lockIntent = new Intent(context, LockScreenActivity.class); - lockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(lockIntent); - } - } - }; - private QueueSaveHandler queueSaveHandler; - private HandlerThread queueSaveHandlerThread; - private boolean queuesRestored; - private int repeatMode; - private int shuffleMode; - private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); - private final BroadcastReceiver bluetoothReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action) - && PreferenceUtil.INSTANCE.isBluetoothSpeaker()) { - if (VERSION.SDK_INT >= VERSION_CODES.M) { - if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) { - play(); + }; + private QueueSaveHandler queueSaveHandler; + private HandlerThread queueSaveHandlerThread; + private boolean queuesRestored; + private int repeatMode; + private int shuffleMode; + private SongPlayCountHelper songPlayCountHelper = new SongPlayCountHelper(); + private final BroadcastReceiver bluetoothReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + String action = intent.getAction(); + if (action != null) { + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action) + && PreferenceUtil.INSTANCE.isBluetoothSpeaker()) { + if (VERSION.SDK_INT >= VERSION_CODES.M) { + if (getAudioManager().getDevices(AudioManager.GET_DEVICES_OUTPUTS).length > 0) { + play(); + } + } else { + if (getAudioManager().isBluetoothA2dpOn()) { + play(); + } + } + } + } } - } else { - if (getAudioManager().isBluetoothA2dpOn()) { - play(); + }; + private PhoneStateListener phoneStateListener = + new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String incomingNumber) { + switch (state) { + case TelephonyManager.CALL_STATE_IDLE: + // Not in call: Play music + play(); + break; + case TelephonyManager.CALL_STATE_RINGING: + case TelephonyManager.CALL_STATE_OFFHOOK: + // A call is dialing, active or on hold + pause(); + break; + default: + } + super.onCallStateChanged(state, incomingNumber); } - } - } - } + }; + private BroadcastReceiver headsetReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action != null) { + if (Intent.ACTION_HEADSET_PLUG.equals(action)) { + int state = intent.getIntExtra("state", -1); + switch (state) { + case 0: + pause(); + break; + case 1: + play(); + break; + } + } + } + } + }; + private ThrottledSeekHandler throttledSeekHandler; + private Handler uiThreadHandler; + private PowerManager.WakeLock wakeLock; + + private static Bitmap copy(Bitmap bitmap) { + Bitmap.Config config = bitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.RGB_565; } - }; - private PhoneStateListener phoneStateListener = - new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String incomingNumber) { - switch (state) { - case TelephonyManager.CALL_STATE_IDLE: - // Not in call: Play music - play(); - break; - case TelephonyManager.CALL_STATE_RINGING: - case TelephonyManager.CALL_STATE_OFFHOOK: - // A call is dialing, active or on hold - pause(); - break; - default: - } - super.onCallStateChanged(state, incomingNumber); + try { + return bitmap.copy(config, false); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + return null; } - }; - private BroadcastReceiver headsetReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action != null) { - if (Intent.ACTION_HEADSET_PLUG.equals(action)) { - int state = intent.getIntExtra("state", -1); - switch (state) { - case 0: - pause(); - break; - case 1: - play(); - break; - } - } - } + } + + private static String getTrackUri(@NonNull Song song) { + return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); + } + + @Override + public void onCreate() { + super.onCreate(); + final TelephonyManager telephonyManager = + (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + if (telephonyManager != null) { + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); } - }; - private ThrottledSeekHandler throttledSeekHandler; - private Handler uiThreadHandler; - private PowerManager.WakeLock wakeLock; - private static Bitmap copy(Bitmap bitmap) { - Bitmap.Config config = bitmap.getConfig(); - if (config == null) { - config = Bitmap.Config.RGB_565; - } - try { - return bitmap.copy(config, false); - } catch (OutOfMemoryError e) { - e.printStackTrace(); - return null; - } - } - - private static String getTrackUri(@NonNull Song song) { - return MusicUtil.INSTANCE.getSongFileUri(song.getId()).toString(); - } - - @Override - public void onCreate() { - super.onCreate(); - final TelephonyManager telephonyManager = - (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); - } - - final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (powerManager != null) { - wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); - } - wakeLock.setReferenceCounted(false); - - musicPlayerHandlerThread = new HandlerThread("PlaybackHandler"); - musicPlayerHandlerThread.start(); - playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); - - playback = new MultiPlayer(this); - playback.setCallbacks(this); - - setupMediaSession(); - - // queue saving needs to run on a separate thread so that it doesn't block the playback handler - // events - queueSaveHandlerThread = - new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND); - queueSaveHandlerThread.start(); - queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper()); - - uiThreadHandler = new Handler(); - - registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE)); - registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); - registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); - - initNotification(); - - mediaStoreObserver = new MediaStoreObserver(this, playerHandler); - throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); - - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - getContentResolver() - .registerContentObserver( - MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); - - PreferenceUtil.INSTANCE.registerOnSharedPreferenceChangedListener(this); - - restoreState(); - - sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED")); - - registerHeadsetEvents(); - registerBluetoothConnected(); - } - - @Override - public void onDestroy() { - unregisterReceiver(widgetIntentReceiver); - unregisterReceiver(updateFavoriteReceiver); - unregisterReceiver(lockScreenReceiver); - if (becomingNoisyReceiverRegistered) { - unregisterReceiver(becomingNoisyReceiver); - becomingNoisyReceiverRegistered = false; - } - if (headsetReceiverRegistered) { - unregisterReceiver(headsetReceiver); - headsetReceiverRegistered = false; - } - if (bluetoothConnectedRegistered) { - unregisterReceiver(bluetoothReceiver); - bluetoothConnectedRegistered = false; - } - mediaSession.setActive(false); - quit(); - releaseResources(); - getContentResolver().unregisterContentObserver(mediaStoreObserver); - PreferenceUtil.INSTANCE.unregisterOnSharedPreferenceChangedListener(this); - wakeLock.release(); - - sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); - } - - public void acquireWakeLock(long milli) { - wakeLock.acquire(milli); - } - - public void addSong(int position, Song song) { - playingQueue.add(position, song); - originalPlayingQueue.add(position, song); - notifyChange(QUEUE_CHANGED); - } - - public void addSong(Song song) { - playingQueue.add(song); - originalPlayingQueue.add(song); - notifyChange(QUEUE_CHANGED); - } - - public void addSongs(int position, List songs) { - playingQueue.addAll(position, songs); - originalPlayingQueue.addAll(position, songs); - notifyChange(QUEUE_CHANGED); - } - - public void addSongs(List songs) { - playingQueue.addAll(songs); - originalPlayingQueue.addAll(songs); - notifyChange(QUEUE_CHANGED); - } - - public void back(boolean force) { - if (getSongProgressMillis() > 2000) { - seek(0); - } else { - playPreviousSong(force); - } - } - - public void clearQueue() { - playingQueue.clear(); - originalPlayingQueue.clear(); - - setPosition(-1); - notifyChange(QUEUE_CHANGED); - } - - public void cycleRepeatMode() { - switch (getRepeatMode()) { - case REPEAT_MODE_NONE: - setRepeatMode(REPEAT_MODE_ALL); - break; - case REPEAT_MODE_ALL: - setRepeatMode(REPEAT_MODE_THIS); - break; - default: - setRepeatMode(REPEAT_MODE_NONE); - break; - } - } - - public int getAudioSessionId() { - if (playback != null) { - return playback.getAudioSessionId(); - } - return -1; - } - - @NonNull - public Song getCurrentSong() { - return getSongAt(getPosition()); - } - - @NonNull - public MediaSessionCompat getMediaSession() { - return mediaSession; - } - - public int getNextPosition(boolean force) { - int position = getPosition() + 1; - switch (getRepeatMode()) { - case REPEAT_MODE_ALL: - if (isLastTrack()) { - position = 0; + final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); } - break; - case REPEAT_MODE_THIS: - if (force) { - if (isLastTrack()) { - position = 0; - } - } else { - position -= 1; - } - break; - default: - case REPEAT_MODE_NONE: - if (isLastTrack()) { - position -= 1; - } - break; - } - return position; - } + wakeLock.setReferenceCounted(false); - @Nullable - public List getPlayingQueue() { - return playingQueue; - } + musicPlayerHandlerThread = new HandlerThread("PlaybackHandler"); + musicPlayerHandlerThread.start(); + playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); - public int getPosition() { - return position; - } + playback = new MultiPlayer(this); + playback.setCallbacks(this); - public void setPosition(final int position) { - // handle this on the handlers thread to avoid blocking the ui thread - playerHandler.removeMessages(SET_POSITION); - playerHandler.obtainMessage(SET_POSITION, position, 0).sendToTarget(); - } + setupMediaSession(); - public int getPreviousPosition(boolean force) { - int newPosition = getPosition() - 1; - switch (repeatMode) { - case REPEAT_MODE_ALL: - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } - } - break; - case REPEAT_MODE_THIS: - if (force) { - if (newPosition < 0) { - if (getPlayingQueue() != null) { - newPosition = getPlayingQueue().size() - 1; - } - } - } else { - newPosition = getPosition(); - } - break; - default: - case REPEAT_MODE_NONE: - if (newPosition < 0) { - newPosition = 0; - } - break; - } - return newPosition; - } + // queue saving needs to run on a separate thread so that it doesn't block the playback handler + // events + queueSaveHandlerThread = + new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND); + queueSaveHandlerThread.start(); + queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper()); - public long getQueueDurationMillis(int position) { - long duration = 0; - for (int i = position + 1; i < playingQueue.size(); i++) { - duration += playingQueue.get(i).getDuration(); - } - return duration; - } + uiThreadHandler = new Handler(); - public int getRepeatMode() { - return repeatMode; - } + registerReceiver(widgetIntentReceiver, new IntentFilter(APP_WIDGET_UPDATE)); + registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); + registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); - public void setRepeatMode(final int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_NONE: - case REPEAT_MODE_ALL: - case REPEAT_MODE_THIS: - this.repeatMode = repeatMode; - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putInt(SAVED_REPEAT_MODE, repeatMode) - .apply(); - prepareNext(); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - break; - } - } - - public int getShuffleMode() { - return shuffleMode; - } - - public void setShuffleMode(final int shuffleMode) { - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putInt(SAVED_SHUFFLE_MODE, shuffleMode) - .apply(); - switch (shuffleMode) { - case SHUFFLE_MODE_SHUFFLE: - this.shuffleMode = shuffleMode; - if (this.getPlayingQueue() != null) { - ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); - } - position = 0; - break; - case SHUFFLE_MODE_NONE: - this.shuffleMode = shuffleMode; - long currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); - playingQueue = new ArrayList<>(originalPlayingQueue); - int newPosition = 0; - if (getPlayingQueue() != null) { - for (Song song : getPlayingQueue()) { - if (song.getId() == currentSongId) { - newPosition = getPlayingQueue().indexOf(song); - } - } - } - position = newPosition; - break; - } - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - notifyChange(QUEUE_CHANGED); - } - - @NonNull - public Song getSongAt(int position) { - if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { - return getPlayingQueue().get(position); - } else { - return Song.Companion.getEmptySong(); - } - } - - public int getSongDurationMillis() { - if (playback != null) { - return playback.duration(); - } - return -1; - } - - public int getSongProgressMillis() { - if (playback != null) { - return playback.position(); - } - return -1; - } - - public void handleAndSendChangeInternal(@NonNull final String what) { - handleChangeInternal(what); - sendChangeInternal(what); - } - - public void initNotification() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && !PreferenceUtil.INSTANCE.isClassicNotification()) { - playingNotification = new PlayingNotificationImpl(); - } else { - playingNotification = new PlayingNotificationOreo(); - } - playingNotification.init(this); - } - - public boolean isLastTrack() { - if (getPlayingQueue() != null) { - return getPosition() == getPlayingQueue().size() - 1; - } - return false; - } - - public boolean isPausedByTransientLossOfFocus() { - return pausedByTransientLossOfFocus; - } - - public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { - this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; - } - - public boolean isPlaying() { - return playback != null && playback.isPlaying(); - } - - public void moveSong(int from, int to) { - if (from == to) { - return; - } - final int currentPosition = getPosition(); - Song songToMove = playingQueue.remove(from); - playingQueue.add(to, songToMove); - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - Song tmpSong = originalPlayingQueue.remove(from); - originalPlayingQueue.add(to, tmpSong); - } - if (from > currentPosition && to <= currentPosition) { - position = currentPosition + 1; - } else if (from < currentPosition && to >= currentPosition) { - position = currentPosition - 1; - } else if (from == currentPosition) { - position = to; - } - notifyChange(QUEUE_CHANGED); - } - - public void notifyChange(@NonNull final String what) { - handleAndSendChangeInternal(what); - sendPublicIntent(what); - } - - @NonNull - @Override - public IBinder onBind(Intent intent) { - return musicBind; - } - - @Override - public void onSharedPreferenceChanged( - @NonNull SharedPreferences sharedPreferences, @NonNull String key) { - switch (key) { - case GAP_LESS_PLAYBACK: - if (sharedPreferences.getBoolean(key, false)) { - prepareNext(); - } else { - if (playback != null) { - playback.setNextDataSource(null); - } - } - break; - case ALBUM_ART_ON_LOCK_SCREEN: - case BLURRED_ALBUM_ART: - updateMediaSessionMetaData(); - break; - case COLORED_NOTIFICATION: - updateNotification(); - break; - case CLASSIC_NOTIFICATION: initNotification(); - updateNotification(); - break; - case TOGGLE_HEADSET: + + mediaStoreObserver = new MediaStoreObserver(this, playerHandler); + throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mediaStoreObserver); + + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + getContentResolver() + .registerContentObserver( + MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI, true, mediaStoreObserver); + + PreferenceUtil.INSTANCE.registerOnSharedPreferenceChangedListener(this); + + restoreState(); + + sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED")); + registerHeadsetEvents(); - break; - } - } - - @Override - public int onStartCommand(@Nullable Intent intent, int flags, int startId) { - if (intent != null && intent.getAction() != null) { - restoreQueuesAndPositionIfNecessary(); - String action = intent.getAction(); - switch (action) { - case ACTION_TOGGLE_PAUSE: - if (isPlaying()) { - pause(); - } else { - play(); - } - break; - case ACTION_PAUSE: - pause(); - break; - case ACTION_PLAY: - play(); - break; - case ACTION_PLAY_PLAYLIST: - playFromPlaylist(intent); - break; - case ACTION_REWIND: - back(true); - break; - case ACTION_SKIP: - playNextSong(true); - break; - case ACTION_STOP: - case ACTION_QUIT: - pendingQuit = false; - quit(); - break; - case ACTION_PENDING_QUIT: - pendingQuit = true; - break; - case TOGGLE_FAVORITE: - MusicUtil.INSTANCE.toggleFavorite(getApplicationContext(), getCurrentSong()); - break; - } + registerBluetoothConnected(); } - return START_NOT_STICKY; - } - - @Override - public void onTrackEnded() { - acquireWakeLock(30000); - playerHandler.sendEmptyMessage(TRACK_ENDED); - } - - @Override - public void onTrackWentToNext() { - playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); - } - - @Override - public boolean onUnbind(Intent intent) { - if (!isPlaying()) { - stopSelf(); - } - return true; - } - - public void openQueue( - @Nullable final List playingQueue, - final int startPosition, - final boolean startPlaying) { - if (playingQueue != null - && !playingQueue.isEmpty() - && startPosition >= 0 - && startPosition < playingQueue.size()) { - // it is important to copy the playing queue here first as we might add/remove songs later - originalPlayingQueue = new ArrayList<>(playingQueue); - this.playingQueue = new ArrayList<>(originalPlayingQueue); - - int position = startPosition; - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - ShuffleHelper.INSTANCE.makeShuffleList(this.playingQueue, startPosition); - position = 0; - } - if (startPlaying) { - playSongAt(position); - } else { - setPosition(position); - } - notifyChange(QUEUE_CHANGED); - } - } - - public boolean openTrackAndPrepareNextAt(int position) { - synchronized (this) { - this.position = position; - boolean prepared = openCurrent(); - if (prepared) { - prepareNextImpl(); - } - notifyChange(META_CHANGED); - notHandledMetaChangedForCurrentTrack = false; - return prepared; - } - } - - public void pause() { - pausedByTransientLossOfFocus = false; - if (playback != null && playback.isPlaying()) { - playback.pause(); - notifyChange(PLAY_STATE_CHANGED); - } - } - - public void play() { - synchronized (this) { - if (requestFocus()) { - if (playback != null && !playback.isPlaying()) { - if (!playback.isInitialized()) { - playSongAt(getPosition()); - } else { - playback.start(); - if (!becomingNoisyReceiverRegistered) { - registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter); - becomingNoisyReceiverRegistered = true; - } - if (notHandledMetaChangedForCurrentTrack) { - handleChangeInternal(META_CHANGED); - notHandledMetaChangedForCurrentTrack = false; - } - notifyChange(PLAY_STATE_CHANGED); - - // fixes a bug where the volume would stay ducked because the - // AudioManager.AUDIOFOCUS_GAIN event is not sent - playerHandler.removeMessages(DUCK); - playerHandler.sendEmptyMessage(UNDUCK); - } + @Override + public void onDestroy() { + unregisterReceiver(widgetIntentReceiver); + unregisterReceiver(updateFavoriteReceiver); + unregisterReceiver(lockScreenReceiver); + if (becomingNoisyReceiverRegistered) { + unregisterReceiver(becomingNoisyReceiver); + becomingNoisyReceiverRegistered = false; } - } else { - Toast.makeText( - this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT) - .show(); - } + if (headsetReceiverRegistered) { + unregisterReceiver(headsetReceiver); + headsetReceiverRegistered = false; + } + if (bluetoothConnectedRegistered) { + unregisterReceiver(bluetoothReceiver); + bluetoothConnectedRegistered = false; + } + mediaSession.setActive(false); + quit(); + releaseResources(); + getContentResolver().unregisterContentObserver(mediaStoreObserver); + PreferenceUtil.INSTANCE.unregisterOnSharedPreferenceChangedListener(this); + wakeLock.release(); + + sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_DESTROYED")); } - } - public void playNextSong(boolean force) { - playSongAt(getNextPosition(force)); - } - - public void playPreviousSong(boolean force) { - playSongAt(getPreviousPosition(force)); - } - - public void playSongAt(final int position) { - // handle this on the handlers thread to avoid blocking the ui thread - playerHandler.removeMessages(PLAY_SONG); - playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); - } - - public void playSongAtImpl(int position) { - if (openTrackAndPrepareNextAt(position)) { - play(); - } else { - Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT) - .show(); + public void acquireWakeLock(long milli) { + wakeLock.acquire(milli); } - } - public void playSongs(ArrayList songs, int shuffleMode) { - if (songs != null && !songs.isEmpty()) { - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - int startPosition = new Random().nextInt(songs.size()); - openQueue(songs, startPosition, false); - setShuffleMode(shuffleMode); - } else { - openQueue(songs, 0, false); - } - play(); - } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); + public void addSong(int position, Song song) { + playingQueue.add(position, song); + originalPlayingQueue.add(position, song); + notifyChange(QUEUE_CHANGED); } - } - public boolean prepareNextImpl() { - synchronized (this) { - try { - int nextPosition = getNextPosition(false); + public void addSong(Song song) { + playingQueue.add(song); + originalPlayingQueue.add(song); + notifyChange(QUEUE_CHANGED); + } + + public void addSongs(int position, List songs) { + playingQueue.addAll(position, songs); + originalPlayingQueue.addAll(position, songs); + notifyChange(QUEUE_CHANGED); + } + + public void addSongs(List songs) { + playingQueue.addAll(songs); + originalPlayingQueue.addAll(songs); + notifyChange(QUEUE_CHANGED); + } + + public void back(boolean force) { + if (getSongProgressMillis() > 2000) { + seek(0); + } else { + playPreviousSong(force); + } + } + + public void clearQueue() { + playingQueue.clear(); + originalPlayingQueue.clear(); + + setPosition(-1); + notifyChange(QUEUE_CHANGED); + } + + public void cycleRepeatMode() { + switch (getRepeatMode()) { + case REPEAT_MODE_NONE: + setRepeatMode(REPEAT_MODE_ALL); + break; + case REPEAT_MODE_ALL: + setRepeatMode(REPEAT_MODE_THIS); + break; + default: + setRepeatMode(REPEAT_MODE_NONE); + break; + } + } + + public int getAudioSessionId() { if (playback != null) { - playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); + return playback.getAudioSessionId(); } - this.nextPosition = nextPosition; - return true; - } catch (Exception e) { - return false; - } - } - } - - public void quit() { - pause(); - playingNotification.stop(); - - closeAudioEffectSession(); - getAudioManager().abandonAudioFocus(audioFocusListener); - stopSelf(); - } - - public void releaseWakeLock() { - if (wakeLock.isHeld()) { - wakeLock.release(); - } - } - - public void removeSong(int position) { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - playingQueue.remove(position); - originalPlayingQueue.remove(position); - } else { - originalPlayingQueue.remove(playingQueue.remove(position)); - } - - rePosition(position); - - notifyChange(QUEUE_CHANGED); - } - - public void removeSong(@NonNull Song song) { - for (int i = 0; i < playingQueue.size(); i++) { - if (playingQueue.get(i).getId() == song.getId()) { - playingQueue.remove(i); - rePosition(i); - } - } - for (int i = 0; i < originalPlayingQueue.size(); i++) { - if (originalPlayingQueue.get(i).getId() == song.getId()) { - originalPlayingQueue.remove(i); - } - } - notifyChange(QUEUE_CHANGED); - } - - public synchronized void restoreQueuesAndPositionIfNecessary() { - if (!queuesRestored && playingQueue.isEmpty()) { - List restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); - List restoredOriginalQueue = - MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); - int restoredPosition = - PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); - int restoredPositionInTrack = - PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION_IN_TRACK, -1); - - if (restoredQueue.size() > 0 - && restoredQueue.size() == restoredOriginalQueue.size() - && restoredPosition != -1) { - this.originalPlayingQueue = restoredOriginalQueue; - this.playingQueue = restoredQueue; - - position = restoredPosition; - openCurrent(); - prepareNext(); - - if (restoredPositionInTrack > 0) { - seek(restoredPositionInTrack); - } - - notHandledMetaChangedForCurrentTrack = true; - sendChangeInternal(META_CHANGED); - sendChangeInternal(QUEUE_CHANGED); - } - } - queuesRestored = true; - } - - public void runOnUiThread(Runnable runnable) { - uiThreadHandler.post(runnable); - } - - public void savePositionInTrack() { - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()) - .apply(); - } - - public void saveQueuesImpl() { - MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); - } - - public void saveState() { - saveQueues(); - savePosition(); - savePositionInTrack(); - } - - public int seek(int millis) { - synchronized (this) { - try { - int newPosition = 0; - if (playback != null) { - newPosition = playback.seek(millis); - } - throttledSeekHandler.notifySeek(); - return newPosition; - } catch (Exception e) { return -1; - } } - } - - // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch - public void sendPublicIntent(@NonNull final String what) { - final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); - - final Song song = getCurrentSong(); - - if (song != null) { - intent.putExtra("id", song.getId()); - intent.putExtra("artist", song.getArtistName()); - intent.putExtra("album", song.getAlbumName()); - intent.putExtra("track", song.getTitle()); - intent.putExtra("duration", song.getDuration()); - intent.putExtra("position", (long) getSongProgressMillis()); - intent.putExtra("playing", isPlaying()); - intent.putExtra("scrobbling_source", RETRO_MUSIC_PACKAGE_NAME); - sendStickyBroadcast(intent); - } - } - - public void toggleShuffle() { - if (getShuffleMode() == SHUFFLE_MODE_NONE) { - setShuffleMode(SHUFFLE_MODE_SHUFFLE); - } else { - setShuffleMode(SHUFFLE_MODE_NONE); - } - } - - public void updateMediaSessionPlaybackState() { - PlaybackStateCompat.Builder stateBuilder = - new PlaybackStateCompat.Builder() - .setActions(MEDIA_SESSION_ACTIONS) - .setState( - isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, - getSongProgressMillis(), - 1); - - setCustomAction(stateBuilder); - - mediaSession.setPlaybackState(stateBuilder.build()); - } - - public void updateNotification() { - if (playingNotification != null && getCurrentSong().getId() != -1) { - playingNotification.update(); - } - } - - void updateMediaSessionMetaData() { - final Song song = getCurrentSong(); - - if (song.getId() == -1) { - mediaSession.setMetadata(null); - return; - } - - final MediaMetadataCompat.Builder metaData = - new MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtistName()) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtistName()) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbumName()) - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()) - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration()) - .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getPosition() + 1) - .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.getYear()) - .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null) - .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); - - if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) { - final Point screenSize = RetroUtil.getScreenSize(MusicService.this); - final BitmapRequestBuilder request = - SongGlideRequest.Builder.from(Glide.with(MusicService.this), song) - .checkIgnoreMediaStore(MusicService.this) - .asBitmap() - .build(); - if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) { - request.transform(new BlurTransformation.Builder(MusicService.this).build()); - } - runOnUiThread( - new Runnable() { - @Override - public void run() { - request.into( - new SimpleTarget(screenSize.x, screenSize.y) { - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - super.onLoadFailed(e, errorDrawable); - mediaSession.setMetadata(metaData.build()); - } - - @Override - public void onResourceReady( - Bitmap resource, GlideAnimation glideAnimation) { - metaData.putBitmap( - MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); - mediaSession.setMetadata(metaData.build()); - } - }); - } - }); - } else { - mediaSession.setMetadata(metaData.build()); - } - } - - private void closeAudioEffectSession() { - final Intent audioEffectsIntent = - new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - if (playback != null) { - audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); - } - audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(audioEffectsIntent); - } - - private AudioManager getAudioManager() { - if (audioManager == null) { - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - } - return audioManager; - } - - private void handleChangeInternal(@NonNull final String what) { - switch (what) { - case PLAY_STATE_CHANGED: - updateNotification(); - updateMediaSessionPlaybackState(); - final boolean isPlaying = isPlaying(); - if (!isPlaying && getSongProgressMillis() > 0) { - savePositionInTrack(); - } - songPlayCountHelper.notifyPlayStateChanged(isPlaying); - break; - case FAVORITE_STATE_CHANGED: - case META_CHANGED: - updateNotification(); - updateMediaSessionMetaData(); - savePosition(); - savePositionInTrack(); - final Song currentSong = getCurrentSong(); - if (currentSong != null) { - HistoryStore.getInstance(this).addSongId(currentSong.getId()); - } - if (songPlayCountHelper.shouldBumpPlayCount()) { - SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId()); - } - if (currentSong != null) { - songPlayCountHelper.notifySongChanged(currentSong); - } - break; - case QUEUE_CHANGED: - updateMediaSessionMetaData(); // because playing queue size might have changed - saveState(); - if (playingQueue.size() > 0) { - prepareNext(); - } else { - playingNotification.stop(); - } - break; - } - } - - private boolean openCurrent() { - synchronized (this) { - try { - if (playback != null) { - return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); - } - } catch (Exception e) { - return false; - } - } - return false; - } - - private void playFromPlaylist(Intent intent) { - Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); - int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); - if (playlist != null) { - List playlistSongs = playlist.getSongs(); - if (!playlistSongs.isEmpty()) { - if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { - int startPosition = new Random().nextInt(playlistSongs.size()); - openQueue(playlistSongs, startPosition, true); - setShuffleMode(shuffleMode); - } else { - openQueue(playlistSongs, 0, true); - } - } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG) - .show(); - } - } else { - Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); - } - } - - private void prepareNext() { - playerHandler.removeMessages(PREPARE_NEXT); - playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); - } - - private void rePosition(int deletedPosition) { - int currentPosition = getPosition(); - if (deletedPosition < currentPosition) { - position = currentPosition - 1; - } else if (deletedPosition == currentPosition) { - if (playingQueue.size() > deletedPosition) { - setPosition(position); - } else { - setPosition(position - 1); - } - } - } - - private void registerBluetoothConnected() { - Log.i(TAG, "registerBluetoothConnected: "); - if (!bluetoothConnectedRegistered) { - registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter); - bluetoothConnectedRegistered = true; - } - } - - private void registerHeadsetEvents() { - if (!headsetReceiverRegistered && PreferenceUtil.INSTANCE.isHeadsetPlugged()) { - registerReceiver(headsetReceiver, headsetReceiverIntentFilter); - headsetReceiverRegistered = true; - } - } - - private void releaseResources() { - playerHandler.removeCallbacksAndMessages(null); - musicPlayerHandlerThread.quitSafely(); - queueSaveHandler.removeCallbacksAndMessages(null); - queueSaveHandlerThread.quitSafely(); - if (playback != null) { - playback.release(); - } - playback = null; - mediaSession.release(); - } - - private boolean requestFocus() { - return (getAudioManager() - .requestAudioFocus( - audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) - == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - } - - private void restoreState() { - shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); - repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); - handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); - handleAndSendChangeInternal(REPEAT_MODE_CHANGED); - - playerHandler.removeMessages(RESTORE_QUEUES); - playerHandler.sendEmptyMessage(RESTORE_QUEUES); - } - - private void savePosition() { - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putInt(SAVED_POSITION, getPosition()) - .apply(); - } - - private void saveQueues() { - queueSaveHandler.removeMessages(SAVE_QUEUES); - queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); - } - - private void sendChangeInternal(final String what) { - sendBroadcast(new Intent(what)); - appWidgetBig.notifyChange(this, what); - appWidgetClassic.notifyChange(this, what); - appWidgetSmall.notifyChange(this, what); - appWidgetCard.notifyChange(this, what); - appWidgetText.notifyChange(this, what); - } - - private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { - int repeatIcon = R.drawable.ic_repeat; // REPEAT_MODE_NONE - if (getRepeatMode() == REPEAT_MODE_THIS) { - repeatIcon = R.drawable.ic_repeat_one; - } else if (getRepeatMode() == REPEAT_MODE_ALL) { - repeatIcon = R.drawable.ic_repeat_white_circle; - } - stateBuilder.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - CYCLE_REPEAT, getString(R.string.action_cycle_repeat), repeatIcon) - .build()); - - final int shuffleIcon = - getShuffleMode() == SHUFFLE_MODE_NONE - ? R.drawable.ic_shuffle_off_circled - : R.drawable.ic_shuffle_on_circled; - stateBuilder.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - TOGGLE_SHUFFLE, getString(R.string.action_toggle_shuffle), shuffleIcon) - .build()); - - final int favoriteIcon = - MusicUtil.INSTANCE.isFavorite(getApplicationContext(), getCurrentSong()) - ? R.drawable.ic_favorite - : R.drawable.ic_favorite_border; - stateBuilder.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - TOGGLE_FAVORITE, getString(R.string.action_toggle_favorite), favoriteIcon) - .build()); - } - - private void setupMediaSession() { - ComponentName mediaButtonReceiverComponentName = - new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class); - - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); - - PendingIntent mediaButtonReceiverPendingIntent = - PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); - - mediaSession = - new MediaSessionCompat( - this, - "RetroMusicPlayer", - mediaButtonReceiverComponentName, - mediaButtonReceiverPendingIntent); - MediaSessionCallback mediasessionCallback = - new MediaSessionCallback(getApplicationContext(), this); - mediaSession.setFlags( - MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); - mediaSession.setCallback(mediasessionCallback); - mediaSession.setActive(true); - mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); - } - - public class MusicBinder extends Binder { @NonNull - public MusicService getService() { - return MusicService.this; + public Song getCurrentSong() { + return getSongAt(getPosition()); + } + + @NonNull + public MediaSessionCompat getMediaSession() { + return mediaSession; + } + + public int getNextPosition(boolean force) { + int position = getPosition() + 1; + switch (getRepeatMode()) { + case REPEAT_MODE_ALL: + if (isLastTrack()) { + position = 0; + } + break; + case REPEAT_MODE_THIS: + if (force) { + if (isLastTrack()) { + position = 0; + } + } else { + position -= 1; + } + break; + default: + case REPEAT_MODE_NONE: + if (isLastTrack()) { + position -= 1; + } + break; + } + return position; + } + + @Nullable + public List getPlayingQueue() { + return playingQueue; + } + + public int getPosition() { + return position; + } + + public void setPosition(final int position) { + // handle this on the handlers thread to avoid blocking the ui thread + playerHandler.removeMessages(SET_POSITION); + playerHandler.obtainMessage(SET_POSITION, position, 0).sendToTarget(); + } + + public int getPreviousPosition(boolean force) { + int newPosition = getPosition() - 1; + switch (repeatMode) { + case REPEAT_MODE_ALL: + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } + } + break; + case REPEAT_MODE_THIS: + if (force) { + if (newPosition < 0) { + if (getPlayingQueue() != null) { + newPosition = getPlayingQueue().size() - 1; + } + } + } else { + newPosition = getPosition(); + } + break; + default: + case REPEAT_MODE_NONE: + if (newPosition < 0) { + newPosition = 0; + } + break; + } + return newPosition; + } + + public long getQueueDurationMillis(int position) { + long duration = 0; + for (int i = position + 1; i < playingQueue.size(); i++) { + duration += playingQueue.get(i).getDuration(); + } + return duration; + } + + public int getRepeatMode() { + return repeatMode; + } + + public void setRepeatMode(final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_NONE: + case REPEAT_MODE_ALL: + case REPEAT_MODE_THIS: + this.repeatMode = repeatMode; + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_REPEAT_MODE, repeatMode) + .apply(); + prepareNext(); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + break; + } + } + + public int getShuffleMode() { + return shuffleMode; + } + + public void setShuffleMode(final int shuffleMode) { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_SHUFFLE_MODE, shuffleMode) + .apply(); + switch (shuffleMode) { + case SHUFFLE_MODE_SHUFFLE: + this.shuffleMode = shuffleMode; + if (this.getPlayingQueue() != null) { + ShuffleHelper.INSTANCE.makeShuffleList(this.getPlayingQueue(), getPosition()); + } + position = 0; + break; + case SHUFFLE_MODE_NONE: + this.shuffleMode = shuffleMode; + long currentSongId = Objects.requireNonNull(getCurrentSong()).getId(); + playingQueue = new ArrayList<>(originalPlayingQueue); + int newPosition = 0; + if (getPlayingQueue() != null) { + for (Song song : getPlayingQueue()) { + if (song.getId() == currentSongId) { + newPosition = getPlayingQueue().indexOf(song); + } + } + } + position = newPosition; + break; + } + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + notifyChange(QUEUE_CHANGED); + } + + @NonNull + public Song getSongAt(int position) { + if (position >= 0 && getPlayingQueue() != null && position < getPlayingQueue().size()) { + return getPlayingQueue().get(position); + } else { + return Song.Companion.getEmptySong(); + } + } + + public int getSongDurationMillis() { + if (playback != null) { + return playback.duration(); + } + return -1; + } + + public int getSongProgressMillis() { + if (playback != null) { + return playback.position(); + } + return -1; + } + + public void handleAndSendChangeInternal(@NonNull final String what) { + handleChangeInternal(what); + sendChangeInternal(what); + } + + public void initNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && !PreferenceUtil.INSTANCE.isClassicNotification()) { + playingNotification = new PlayingNotificationImpl(); + } else { + playingNotification = new PlayingNotificationOreo(); + } + playingNotification.init(this); + } + + public boolean isLastTrack() { + if (getPlayingQueue() != null) { + return getPosition() == getPlayingQueue().size() - 1; + } + return false; + } + + public boolean isPausedByTransientLossOfFocus() { + return pausedByTransientLossOfFocus; + } + + public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { + this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; + } + + public boolean isPlaying() { + return playback != null && playback.isPlaying(); + } + + public void moveSong(int from, int to) { + if (from == to) { + return; + } + final int currentPosition = getPosition(); + Song songToMove = playingQueue.remove(from); + playingQueue.add(to, songToMove); + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + Song tmpSong = originalPlayingQueue.remove(from); + originalPlayingQueue.add(to, tmpSong); + } + if (from > currentPosition && to <= currentPosition) { + position = currentPosition + 1; + } else if (from < currentPosition && to >= currentPosition) { + position = currentPosition - 1; + } else if (from == currentPosition) { + position = to; + } + notifyChange(QUEUE_CHANGED); + } + + public void notifyChange(@NonNull final String what) { + handleAndSendChangeInternal(what); + sendPublicIntent(what); + } + + @NonNull + @Override + public IBinder onBind(Intent intent) { + return musicBind; + } + + @Override + public void onSharedPreferenceChanged( + @NonNull SharedPreferences sharedPreferences, @NonNull String key) { + switch (key) { + case GAP_LESS_PLAYBACK: + if (sharedPreferences.getBoolean(key, false)) { + prepareNext(); + } else { + if (playback != null) { + playback.setNextDataSource(null); + } + } + break; + case ALBUM_ART_ON_LOCK_SCREEN: + case BLURRED_ALBUM_ART: + updateMediaSessionMetaData(); + break; + case COLORED_NOTIFICATION: + updateNotification(); + break; + case CLASSIC_NOTIFICATION: + initNotification(); + updateNotification(); + break; + case TOGGLE_HEADSET: + registerHeadsetEvents(); + break; + } + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + restoreQueuesAndPositionIfNecessary(); + String action = intent.getAction(); + switch (action) { + case ACTION_TOGGLE_PAUSE: + if (isPlaying()) { + pause(); + } else { + play(); + } + break; + case ACTION_PAUSE: + pause(); + break; + case ACTION_PLAY: + play(); + break; + case ACTION_PLAY_PLAYLIST: + playFromPlaylist(intent); + break; + case ACTION_REWIND: + back(true); + break; + case ACTION_SKIP: + playNextSong(true); + break; + case ACTION_STOP: + case ACTION_QUIT: + pendingQuit = false; + quit(); + break; + case ACTION_PENDING_QUIT: + pendingQuit = true; + break; + case TOGGLE_FAVORITE: + MusicUtil.INSTANCE.toggleFavorite(getApplicationContext(), getCurrentSong()); + break; + } + } + + return START_NOT_STICKY; + } + + @Override + public void onTrackEnded() { + acquireWakeLock(30000); + playerHandler.sendEmptyMessage(TRACK_ENDED); + } + + @Override + public void onTrackWentToNext() { + playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); + } + + @Override + public boolean onUnbind(Intent intent) { + if (!isPlaying()) { + stopSelf(); + } + return true; + } + + public void openQueue( + @Nullable final List playingQueue, + final int startPosition, + final boolean startPlaying) { + if (playingQueue != null + && !playingQueue.isEmpty() + && startPosition >= 0 + && startPosition < playingQueue.size()) { + // it is important to copy the playing queue here first as we might add/remove songs later + originalPlayingQueue = new ArrayList<>(playingQueue); + this.playingQueue = new ArrayList<>(originalPlayingQueue); + + int position = startPosition; + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + ShuffleHelper.INSTANCE.makeShuffleList(this.playingQueue, startPosition); + position = 0; + } + if (startPlaying) { + playSongAt(position); + } else { + setPosition(position); + } + notifyChange(QUEUE_CHANGED); + } + } + + public boolean openTrackAndPrepareNextAt(int position) { + synchronized (this) { + this.position = position; + boolean prepared = openCurrent(); + if (prepared) { + prepareNextImpl(); + } + notifyChange(META_CHANGED); + notHandledMetaChangedForCurrentTrack = false; + return prepared; + } + } + + public void pause() { + pausedByTransientLossOfFocus = false; + if (playback != null && playback.isPlaying()) { + playback.pause(); + notifyChange(PLAY_STATE_CHANGED); + } + } + + public void play() { + synchronized (this) { + if (requestFocus()) { + if (playback != null && !playback.isPlaying()) { + if (!playback.isInitialized()) { + playSongAt(getPosition()); + } else { + playback.start(); + if (!becomingNoisyReceiverRegistered) { + registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter); + becomingNoisyReceiverRegistered = true; + } + if (notHandledMetaChangedForCurrentTrack) { + handleChangeInternal(META_CHANGED); + notHandledMetaChangedForCurrentTrack = false; + } + notifyChange(PLAY_STATE_CHANGED); + + // fixes a bug where the volume would stay ducked because the + // AudioManager.AUDIOFOCUS_GAIN event is not sent + playerHandler.removeMessages(DUCK); + playerHandler.sendEmptyMessage(UNDUCK); + } + } + } else { + Toast.makeText( + this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT) + .show(); + } + } + } + + public void playNextSong(boolean force) { + playSongAt(getNextPosition(force)); + } + + public void playPreviousSong(boolean force) { + playSongAt(getPreviousPosition(force)); + } + + public void playSongAt(final int position) { + // handle this on the handlers thread to avoid blocking the ui thread + playerHandler.removeMessages(PLAY_SONG); + playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); + } + + public void playSongAtImpl(int position) { + if (openTrackAndPrepareNextAt(position)) { + play(); + } else { + Toast.makeText(this, getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT) + .show(); + } + } + + public void playSongs(ArrayList songs, int shuffleMode) { + if (songs != null && !songs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = new Random().nextInt(songs.size()); + openQueue(songs, startPosition, false); + setShuffleMode(shuffleMode); + } else { + openQueue(songs, 0, false); + } + play(); + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); + } + } + + public boolean prepareNextImpl() { + synchronized (this) { + try { + int nextPosition = getNextPosition(false); + if (playback != null) { + playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition)))); + } + this.nextPosition = nextPosition; + return true; + } catch (Exception e) { + return false; + } + } + } + + public void quit() { + pause(); + playingNotification.stop(); + + closeAudioEffectSession(); + getAudioManager().abandonAudioFocus(audioFocusListener); + stopSelf(); + } + + public void releaseWakeLock() { + if (wakeLock.isHeld()) { + wakeLock.release(); + } + } + + public void removeSong(int position) { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + playingQueue.remove(position); + originalPlayingQueue.remove(position); + } else { + originalPlayingQueue.remove(playingQueue.remove(position)); + } + + rePosition(position); + + notifyChange(QUEUE_CHANGED); + } + + public void removeSong(@NonNull Song song) { + for (int i = 0; i < playingQueue.size(); i++) { + if (playingQueue.get(i).getId() == song.getId()) { + playingQueue.remove(i); + rePosition(i); + } + } + for (int i = 0; i < originalPlayingQueue.size(); i++) { + if (originalPlayingQueue.get(i).getId() == song.getId()) { + originalPlayingQueue.remove(i); + } + } + notifyChange(QUEUE_CHANGED); + } + + public synchronized void restoreQueuesAndPositionIfNecessary() { + if (!queuesRestored && playingQueue.isEmpty()) { + List restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); + List restoredOriginalQueue = + MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); + int restoredPosition = + PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); + int restoredPositionInTrack = + PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION_IN_TRACK, -1); + + if (restoredQueue.size() > 0 + && restoredQueue.size() == restoredOriginalQueue.size() + && restoredPosition != -1) { + this.originalPlayingQueue = restoredOriginalQueue; + this.playingQueue = restoredQueue; + + position = restoredPosition; + openCurrent(); + prepareNext(); + + if (restoredPositionInTrack > 0) { + seek(restoredPositionInTrack); + } + + notHandledMetaChangedForCurrentTrack = true; + sendChangeInternal(META_CHANGED); + sendChangeInternal(QUEUE_CHANGED); + } + } + queuesRestored = true; + } + + public void runOnUiThread(Runnable runnable) { + uiThreadHandler.post(runnable); + } + + public void savePositionInTrack() { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()) + .apply(); + } + + public void saveQueuesImpl() { + MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); + } + + public void saveState() { + saveQueues(); + savePosition(); + savePositionInTrack(); + } + + public int seek(int millis) { + synchronized (this) { + try { + int newPosition = 0; + if (playback != null) { + newPosition = playback.seek(millis); + } + throttledSeekHandler.notifySeek(); + return newPosition; + } catch (Exception e) { + return -1; + } + } + } + + // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch + public void sendPublicIntent(@NonNull final String what) { + final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); + + final Song song = getCurrentSong(); + + if (song != null) { + intent.putExtra("id", song.getId()); + intent.putExtra("artist", song.getArtistName()); + intent.putExtra("album", song.getAlbumName()); + intent.putExtra("track", song.getTitle()); + intent.putExtra("duration", song.getDuration()); + intent.putExtra("position", (long) getSongProgressMillis()); + intent.putExtra("playing", isPlaying()); + intent.putExtra("scrobbling_source", RETRO_MUSIC_PACKAGE_NAME); + sendStickyBroadcast(intent); + } + } + + public void toggleShuffle() { + if (getShuffleMode() == SHUFFLE_MODE_NONE) { + setShuffleMode(SHUFFLE_MODE_SHUFFLE); + } else { + setShuffleMode(SHUFFLE_MODE_NONE); + } + } + + public void updateMediaSessionPlaybackState() { + PlaybackStateCompat.Builder stateBuilder = + new PlaybackStateCompat.Builder() + .setActions(MEDIA_SESSION_ACTIONS) + .setState( + isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, + getSongProgressMillis(), + 1); + + setCustomAction(stateBuilder); + + mediaSession.setPlaybackState(stateBuilder.build()); + } + + public void updateNotification() { + if (playingNotification != null && getCurrentSong().getId() != -1) { + playingNotification.update(); + } + } + + public void updateMediaSessionMetaData() { + Log.i(TAG, "onResourceReady: "); + final Song song = getCurrentSong(); + + if (song.getId() == -1) { + mediaSession.setMetadata(null); + return; + } + + final MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.getArtistName()) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.getArtistName()) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.getAlbumName()) + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.getTitle()) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.getDuration()) + .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, getPosition() + 1) + .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.getYear()) + .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, getPlayingQueue().size()); + } + + if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) { + final Point screenSize = RetroUtil.getScreenSize(MusicService.this); + final BitmapRequestBuilder request = SongGlideRequest.Builder.from(Glide.with(MusicService.this), song) + .checkIgnoreMediaStore(MusicService.this) + .asBitmap().build(); + if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) { + request.transform(new BlurTransformation.Builder(MusicService.this).build()); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + request.into(new SimpleTarget(screenSize.x, screenSize.y) { + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + super.onLoadFailed(e, errorDrawable); + mediaSession.setMetadata(metaData.build()); + } + + @Override + public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { + + metaData.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource)); + mediaSession.setMetadata(metaData.build()); + } + }); + } + }); + } else { + mediaSession.setMetadata(metaData.build()); + } + } + + private void closeAudioEffectSession() { + final Intent audioEffectsIntent = + new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + if (playback != null) { + audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); + } + audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(audioEffectsIntent); + } + + private AudioManager getAudioManager() { + if (audioManager == null) { + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + return audioManager; + } + + private void handleChangeInternal(@NonNull final String what) { + switch (what) { + case PLAY_STATE_CHANGED: + updateNotification(); + updateMediaSessionPlaybackState(); + final boolean isPlaying = isPlaying(); + if (!isPlaying && getSongProgressMillis() > 0) { + savePositionInTrack(); + } + songPlayCountHelper.notifyPlayStateChanged(isPlaying); + break; + case FAVORITE_STATE_CHANGED: + case META_CHANGED: + updateNotification(); + updateMediaSessionMetaData(); + savePosition(); + savePositionInTrack(); + final Song currentSong = getCurrentSong(); + HistoryStore.getInstance(this).addSongId(currentSong.getId()); + if (songPlayCountHelper.shouldBumpPlayCount()) { + SongPlayCountStore.getInstance(this).bumpPlayCount(songPlayCountHelper.getSong().getId()); + } + songPlayCountHelper.notifySongChanged(currentSong); + break; + case QUEUE_CHANGED: + updateMediaSessionMetaData(); // because playing queue size might have changed + saveState(); + if (playingQueue.size() > 0) { + prepareNext(); + } else { + playingNotification.stop(); + } + break; + } + } + + private boolean openCurrent() { + synchronized (this) { + try { + if (playback != null) { + return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong()))); + } + } catch (Exception e) { + return false; + } + } + return false; + } + + private void playFromPlaylist(Intent intent) { + Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); + int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); + if (playlist != null) { + List playlistSongs = playlist.getSongs(); + if (!playlistSongs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = new Random().nextInt(playlistSongs.size()); + openQueue(playlistSongs, startPosition, true); + setShuffleMode(shuffleMode); + } else { + openQueue(playlistSongs, 0, true); + } + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG) + .show(); + } + } else { + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); + } + } + + private void prepareNext() { + playerHandler.removeMessages(PREPARE_NEXT); + playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); + } + + private void rePosition(int deletedPosition) { + int currentPosition = getPosition(); + if (deletedPosition < currentPosition) { + position = currentPosition - 1; + } else if (deletedPosition == currentPosition) { + if (playingQueue.size() > deletedPosition) { + setPosition(position); + } else { + setPosition(position - 1); + } + } + } + + private void registerBluetoothConnected() { + Log.i(TAG, "registerBluetoothConnected: "); + if (!bluetoothConnectedRegistered) { + registerReceiver(bluetoothReceiver, bluetoothConnectedIntentFilter); + bluetoothConnectedRegistered = true; + } + } + + private void registerHeadsetEvents() { + if (!headsetReceiverRegistered && PreferenceUtil.INSTANCE.isHeadsetPlugged()) { + registerReceiver(headsetReceiver, headsetReceiverIntentFilter); + headsetReceiverRegistered = true; + } + } + + private void releaseResources() { + playerHandler.removeCallbacksAndMessages(null); + musicPlayerHandlerThread.quitSafely(); + queueSaveHandler.removeCallbacksAndMessages(null); + queueSaveHandlerThread.quitSafely(); + if (playback != null) { + playback.release(); + } + playback = null; + mediaSession.release(); + } + + private boolean requestFocus() { + return (getAudioManager() + .requestAudioFocus( + audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + } + + private void restoreState() { + shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_SHUFFLE_MODE, 0); + repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_REPEAT_MODE, 0); + handleAndSendChangeInternal(SHUFFLE_MODE_CHANGED); + handleAndSendChangeInternal(REPEAT_MODE_CHANGED); + + playerHandler.removeMessages(RESTORE_QUEUES); + playerHandler.sendEmptyMessage(RESTORE_QUEUES); + } + + private void savePosition() { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(SAVED_POSITION, getPosition()) + .apply(); + } + + private void saveQueues() { + queueSaveHandler.removeMessages(SAVE_QUEUES); + queueSaveHandler.sendEmptyMessage(SAVE_QUEUES); + } + + private void sendChangeInternal(final String what) { + sendBroadcast(new Intent(what)); + appWidgetBig.notifyChange(this, what); + appWidgetClassic.notifyChange(this, what); + appWidgetSmall.notifyChange(this, what); + appWidgetCard.notifyChange(this, what); + appWidgetText.notifyChange(this, what); + } + + private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { + int repeatIcon = R.drawable.ic_repeat; // REPEAT_MODE_NONE + if (getRepeatMode() == REPEAT_MODE_THIS) { + repeatIcon = R.drawable.ic_repeat_one; + } else if (getRepeatMode() == REPEAT_MODE_ALL) { + repeatIcon = R.drawable.ic_repeat_white_circle; + } + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + CYCLE_REPEAT, getString(R.string.action_cycle_repeat), repeatIcon) + .build()); + + final int shuffleIcon = + getShuffleMode() == SHUFFLE_MODE_NONE + ? R.drawable.ic_shuffle_off_circled + : R.drawable.ic_shuffle_on_circled; + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + TOGGLE_SHUFFLE, getString(R.string.action_toggle_shuffle), shuffleIcon) + .build()); + + final int favoriteIcon = + MusicUtil.INSTANCE.isFavorite(getApplicationContext(), getCurrentSong()) + ? R.drawable.ic_favorite + : R.drawable.ic_favorite_border; + stateBuilder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + TOGGLE_FAVORITE, getString(R.string.action_toggle_favorite), favoriteIcon) + .build()); + } + + private void setupMediaSession() { + ComponentName mediaButtonReceiverComponentName = + new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class); + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); + + PendingIntent mediaButtonReceiverPendingIntent = + PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); + + mediaSession = + new MediaSessionCompat( + this, + "RetroMusicPlayer", + mediaButtonReceiverComponentName, + mediaButtonReceiverPendingIntent); + MediaSessionCallback mediasessionCallback = + new MediaSessionCallback(getApplicationContext(), this); + mediaSession.setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setCallback(mediasessionCallback); + mediaSession.setActive(true); + mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); + } + + public class MusicBinder extends Binder { + + @NonNull + public MusicService getService() { + return MusicService.this; + } } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt index 7bcbdfc99..d0dd3bebb 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/PreferenceUtil.kt @@ -8,7 +8,75 @@ import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.preference.PreferenceManager import androidx.viewpager.widget.ViewPager -import code.name.monkey.retromusic.* +import code.name.monkey.retromusic.ADAPTIVE_COLOR_APP +import code.name.monkey.retromusic.ALBUM_ARTISTS_ONLY +import code.name.monkey.retromusic.ALBUM_ART_ON_LOCK_SCREEN +import code.name.monkey.retromusic.ALBUM_COVER_STYLE +import code.name.monkey.retromusic.ALBUM_COVER_TRANSFORM +import code.name.monkey.retromusic.ALBUM_DETAIL_SONG_SORT_ORDER +import code.name.monkey.retromusic.ALBUM_GRID_SIZE +import code.name.monkey.retromusic.ALBUM_GRID_SIZE_LAND +import code.name.monkey.retromusic.ALBUM_GRID_STYLE +import code.name.monkey.retromusic.ALBUM_SONG_SORT_ORDER +import code.name.monkey.retromusic.ALBUM_SORT_ORDER +import code.name.monkey.retromusic.ARTIST_ALBUM_SORT_ORDER +import code.name.monkey.retromusic.ARTIST_GRID_SIZE +import code.name.monkey.retromusic.ARTIST_GRID_SIZE_LAND +import code.name.monkey.retromusic.ARTIST_GRID_STYLE +import code.name.monkey.retromusic.ARTIST_SONG_SORT_ORDER +import code.name.monkey.retromusic.ARTIST_SORT_ORDER +import code.name.monkey.retromusic.AUDIO_DUCKING +import code.name.monkey.retromusic.AUTO_DOWNLOAD_IMAGES_POLICY +import code.name.monkey.retromusic.App +import code.name.monkey.retromusic.BLACK_THEME +import code.name.monkey.retromusic.BLUETOOTH_PLAYBACK +import code.name.monkey.retromusic.BLURRED_ALBUM_ART +import code.name.monkey.retromusic.CAROUSEL_EFFECT +import code.name.monkey.retromusic.CHOOSE_EQUALIZER +import code.name.monkey.retromusic.CLASSIC_NOTIFICATION +import code.name.monkey.retromusic.COLORED_APP_SHORTCUTS +import code.name.monkey.retromusic.COLORED_NOTIFICATION +import code.name.monkey.retromusic.DESATURATED_COLOR +import code.name.monkey.retromusic.EXPAND_NOW_PLAYING_PANEL +import code.name.monkey.retromusic.EXTRA_SONG_INFO +import code.name.monkey.retromusic.FILTER_SONG +import code.name.monkey.retromusic.GAP_LESS_PLAYBACK +import code.name.monkey.retromusic.GENERAL_THEME +import code.name.monkey.retromusic.GENRE_SORT_ORDER +import code.name.monkey.retromusic.HOME_ALBUM_GRID_STYLE +import code.name.monkey.retromusic.HOME_ARTIST_GRID_STYLE +import code.name.monkey.retromusic.IGNORE_MEDIA_STORE_ARTWORK +import code.name.monkey.retromusic.INITIALIZED_BLACKLIST +import code.name.monkey.retromusic.KEEP_SCREEN_ON +import code.name.monkey.retromusic.LANGUAGE_NAME +import code.name.monkey.retromusic.LAST_ADDED_CUTOFF +import code.name.monkey.retromusic.LAST_CHANGELOG_VERSION +import code.name.monkey.retromusic.LAST_PAGE +import code.name.monkey.retromusic.LAST_SLEEP_TIMER_VALUE +import code.name.monkey.retromusic.LIBRARY_CATEGORIES +import code.name.monkey.retromusic.LOCK_SCREEN +import code.name.monkey.retromusic.LYRICS_OPTIONS +import code.name.monkey.retromusic.NEXT_SLEEP_TIMER_ELAPSED_REALTIME +import code.name.monkey.retromusic.NOW_PLAYING_SCREEN_ID +import code.name.monkey.retromusic.PAUSE_ON_ZERO_VOLUME +import code.name.monkey.retromusic.PLAYLIST_SORT_ORDER +import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.RECENTLY_PLAYED_CUTOFF +import code.name.monkey.retromusic.SAF_SDCARD_URI +import code.name.monkey.retromusic.SLEEP_TIMER_FINISH_SONG +import code.name.monkey.retromusic.SONG_GRID_SIZE +import code.name.monkey.retromusic.SONG_GRID_SIZE_LAND +import code.name.monkey.retromusic.SONG_GRID_STYLE +import code.name.monkey.retromusic.SONG_SORT_ORDER +import code.name.monkey.retromusic.START_DIRECTORY +import code.name.monkey.retromusic.TAB_TEXT_MODE +import code.name.monkey.retromusic.TOGGLE_ADD_CONTROLS +import code.name.monkey.retromusic.TOGGLE_FULL_SCREEN +import code.name.monkey.retromusic.TOGGLE_HEADSET +import code.name.monkey.retromusic.TOGGLE_HOME_BANNER +import code.name.monkey.retromusic.TOGGLE_SHUFFLE +import code.name.monkey.retromusic.TOGGLE_VOLUME +import code.name.monkey.retromusic.USER_NAME import code.name.monkey.retromusic.extensions.getIntRes import code.name.monkey.retromusic.extensions.getStringOrDefault import code.name.monkey.retromusic.fragments.AlbumCoverStyle @@ -16,7 +84,13 @@ import code.name.monkey.retromusic.fragments.NowPlayingScreen import code.name.monkey.retromusic.fragments.folder.FoldersFragment import code.name.monkey.retromusic.helper.SortOrder.* import code.name.monkey.retromusic.model.CategoryInfo -import code.name.monkey.retromusic.transform.* +import code.name.monkey.retromusic.transform.CascadingPageTransformer +import code.name.monkey.retromusic.transform.DepthTransformation +import code.name.monkey.retromusic.transform.HingeTransformation +import code.name.monkey.retromusic.transform.HorizontalFlipTransformation +import code.name.monkey.retromusic.transform.NormalPageTransformer +import code.name.monkey.retromusic.transform.VerticalFlipTransformation +import code.name.monkey.retromusic.transform.VerticalStackTransformer import code.name.monkey.retromusic.util.theme.ThemeMode import com.google.android.material.bottomnavigation.LabelVisibilityMode import com.google.gson.Gson @@ -154,6 +228,7 @@ object PreferenceUtil { putString(ALBUM_SORT_ORDER, value) } + var artistSortOrder get() = sharedPreferences.getStringOrDefault( ARTIST_SORT_ORDER, @@ -181,6 +256,15 @@ object PreferenceUtil { ArtistAlbumSortOrder.ALBUM_A_Z ) + var playlistSortOrder + get() = sharedPreferences.getStringOrDefault( + PLAYLIST_SORT_ORDER, + PlaylistSortOrder.PLAYLIST_A_Z + ) + set(value) = sharedPreferences.edit { + putString(PLAYLIST_SORT_ORDER, value) + } + val genreSortOrder get() = sharedPreferences.getStringOrDefault( GENRE_SORT_ORDER, @@ -413,7 +497,7 @@ object PreferenceUtil { val homeAlbumGridStyle: Int get() { - val position = sharedPreferences.getStringOrDefault(HOME_ALBUM_GRID_STYLE, "0").toInt() + val position = sharedPreferences.getStringOrDefault(HOME_ALBUM_GRID_STYLE, "4").toInt() val typedArray = App.getContext().resources.obtainTypedArray(R.array.pref_home_grid_style_layout) val layoutRes = typedArray.getResourceId(position, 0) diff --git a/app/src/main/res/drawable-night/ic_launcher_background.xml b/app/src/main/res/drawable-night/ic_launcher_background.xml new file mode 100644 index 000000000..c760e2636 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index da4d42d65..2417b12d2 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -2,70 +2,71 @@ xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" - android:viewportWidth="921.0526" - android:viewportHeight="921.0526"> - - - - - - - - - - - - - - - - - - - - - - - - - - + android:viewportWidth="108" + android:viewportHeight="108"> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index b97f2835e..d651a2a61 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,6 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> diff --git a/app/src/main/res/layout/activity_lyrics.xml b/app/src/main/res/layout/activity_lyrics.xml index 3740113e0..7facebba5 100644 --- a/app/src/main/res/layout/activity_lyrics.xml +++ b/app/src/main/res/layout/activity_lyrics.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" + android:id="@+id/container" android:layout_height="match_parent" android:transitionName="@string/transition_lyrics"> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09bc5..7353dbd1f 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09bc5..7353dbd1f 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 43bb965a936504f9bd3db60c9ad8ba31a4fdd5c6..ad35070726c01bd3486444e3c6bead58daa23516 100644 GIT binary patch delta 3320 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 GhW!tiB7t52 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index a1e8f4e0326469e8b50d6ab9e2803a5be9370c3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2256 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 Syp{j}00{s|MNUMnLSTZrZd14b delta 2193 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 T=)h3`015yANkvXXu0mjfBIGF1 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png deleted file mode 100644 index 41be56878fcd2bfea3e9b7a804d0f373bea5b956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8211 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 O571K6RjpRGj{F}$A5#nf diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 3b2d6ffa530229fbf3f5de8b17393dad63ab45bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1394 zcmZ`(Yg7^j7&QxWxNIgOzRI`w#LCQKrj;*5B@_h9N1Q=rx~UB-P0Lhr_{u2X??+ld zhGnIK;V`GeW#9u*N^sc2)Y?*XijP^pwjZ5yzx$nY&v)IOw${>!8VFSy+#If*L0`|IG7A>RFE{ zem2$L=&h;4xdTE2n$=Fa%Mi5LfCdE&X(}f8(FN)>+HRnuFuKM@sBC}QF)|i@>B5Ve zhl{RTWY(ek>-8Ngxw7Na;d`US-yP8~&G;Hkx&yYGg1w3VLkPW_(>V)}_g8?#vjx$n z?Q1q2A7piG2K5yeK+*)MwCq}pgU+~D3q|+yXL0Y|Vse5kHx*Ao6{A(TM@^BEVS;-^p=Rc9sILP2p=^ONaB=684m;xXsaY+ z_5e8#10U30!Z%Y!<0X@t*-{?NzJ|@5^SIEDzF7n_7a&Ly(yWr)R_7$)__yb^PL0hp zPD_>x6#0BQh@(E4Yhs5CO#fA5UQ1uYk~svUEH<@LFoYw-krt;LKNR_bc914lJVk9@sBd$D4UWsJBi09iK00nZwMY z?sRLDgURsTerT8}(=-A?wR98rb%#clO0@*O&d75XUK@5~nRf3OcxQSy-HT}Rs%xw~ zd(MLi@6GEl!Nk`ZbZyP?BVh<2em&wA+0I5kJAz(XA=0xJGzcnzcKI8YuyVwu)xx8s z#G*4PiG?Cq8Ytk6hjzd%ZCn%sX750pyF4f&m*$lZQhL`cRW}2IgD~Jix)YkNJNzgN zh0KJZtxS=TXb@Hs=M|lTv6}c9nn`HG^Wz;cjF%B2F;0=&*}eEhhtIbfj_!o$Ix7Qi zs9#JE9OHaR?p|E&5a{lM;ML0n?>|XDYiUOIznQ(TRT!Gbo?J*syjQpa`TQ+ziN(X` zB63siZta81>e;0rj&eV{1-l&kv_!ecpn*=PUPLysOEpvqvmOD6Z zdU$ayyOitNH`4OjVdf8PVw5Z@3hs=Jn7{1OwmhEC7jQEH?$!brcQ0=3$qCsN0@WFW z@+nT98*xzvt@vGcO=$-X1X`mEsG&q&M%A(ULi%{z!tkeo%dSrjM!k)!V?X0Jts8yN znuTK~Yggg{bu&F}wsddeu%>GI+9AeApmzcTgqQ~;mOYLAad=(4#UEm;%Bkw#+&E8N z?oN0;Qi1`YlDBg%=n=vQZGsBu(W++Xy1wXkA&m7VpeJ38ih=$Il68OR*Q+Yy`;`^V Y1GpW!Y508hMwhA~y#hU(j+{*U7uEcWq5uE@ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index a8008b182ec74ecfe09001526c3e455d35e63ac8..a5786f83ca909c76986fd3841e82b915e2feb86f 100644 GIT binary patch delta 2192 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 Syp{j}00{s|MNUMnLSTZrZd14b delta 2193 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 T=)h3`015yANkvXXu0mjfBIGF1 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 0fdcb0cb7d15ba50314b41bff47e231dcdaa970f..7b5aa2c9c17fb0976eb89b03ab85d2fe0328d6bd 100644 GIT binary patch literal 4613 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 literal 5189 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 0fdcb0cb7d15ba50314b41bff47e231dcdaa970f..7b5aa2c9c17fb0976eb89b03ab85d2fe0328d6bd 100644 GIT binary patch literal 4613 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 literal 5189 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 XmcK=JO$QQ000000NkvXXu0mjftF+^; diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png deleted file mode 100644 index a42cd921010787221017bab48718845c2ad57e91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34620 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 P`hR6!?taL6S6uqPS0OHD diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 2acd41a3d60ec82372b2cfaae5c770c3eed7cd69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5716 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 XmcK=JO$QQ000000NkvXXu0mjftF+^; diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index fda6f29e1549ce87407f6ad6f1dcbf2919b09808..a57669d6581321ec62e14476a520095a247284e8 100644 GIT binary patch literal 10135 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 KW~DkJ?0*0q_Y?{M diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index 0282220d304c2b716ff629a1e46d0322b5b43819..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50987 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 E0I{N6?EnA( diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f5f397f0684054be1cf13551907cec93e288d645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8527 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 b8`+z?DXB+xXkE*h0uyYk>@7(a$iMy%=9&Ai diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index fda6f29e1549ce87407f6ad6f1dcbf2919b09808..a57669d6581321ec62e14476a520095a247284e8 100644 GIT binary patch literal 10135 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 KW~DkJ?0*0q_Y?{M diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index c5d5899fd..236a57d3a 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #FFFFFF + #1C1C1C \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index e2cad59d7..1020e9a06 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -16,6 +16,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7bb2c04d6..4e7892215 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -891,4 +891,7 @@ Import playlist It imports all playlists listed in the Android Media Store with songs, if the playlists already exists, the songs will get merged. Import + Song count + Ascending + Song count desc diff --git a/app/src/main/res/xml/pref_ui.xml b/app/src/main/res/xml/pref_ui.xml index 27cac1c8b..54af84920 100644 --- a/app/src/main/res/xml/pref_ui.xml +++ b/app/src/main/res/xml/pref_ui.xml @@ -84,14 +84,14 @@ Date: Fri, 9 Oct 2020 23:32:52 +0530 Subject: [PATCH 49/72] Added image to queue --- .../adapter/song/PlayingQueueAdapter.kt | 22 ++++++++++++++----- .../retromusic/adapter/song/SongAdapter.kt | 1 - .../main/res/layout-sw600dp/item_queue.xml | 10 +++++++++ app/src/main/res/layout/item_queue.xml | 9 ++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt index a6e986229..f8fe7ab40 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt @@ -19,6 +19,8 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.glide.RetroMusicColoredTarget +import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicPlayerRemote.isPlaying import code.name.monkey.retromusic.helper.MusicPlayerRemote.playNextSong @@ -26,6 +28,8 @@ import code.name.monkey.retromusic.helper.MusicPlayerRemote.removeFromQueue import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.ViewUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor +import com.bumptech.glide.Glide import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange import com.h6ah4i.android.widget.advrecyclerview.draggable.annotation.DraggableItemStateFlags @@ -55,8 +59,8 @@ class PlayingQueueAdapter( override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) { super.onBindViewHolder(holder, position) - holder.imageText?.text = (position - current).toString() - holder.time?.text = MusicUtil.getReadableDurationString(dataSet[position].duration) + val song = dataSet[position] + holder.time?.text = MusicUtil.getReadableDurationString(song.duration) if (holder.itemViewType == HISTORY || holder.itemViewType == CURRENT) { setAlpha(holder, 0.5f) } @@ -72,7 +76,17 @@ class PlayingQueueAdapter( } override fun loadAlbumCover(song: Song, holder: SongAdapter.ViewHolder) { - // We don't want to load it in this adapter + if (holder.image == null) { + return + } + SongGlideRequest.Builder.from(Glide.with(activity), song) + .checkIgnoreMediaStore(activity) + .generatePalette(activity).build() + .into(object : RetroMusicColoredTarget(holder.image!!) { + override fun onColorReady(colors: MediaNotificationProcessor) { + //setColors(colors, holder) + } + }) } fun swapDataSet(dataSet: List, position: Int) { @@ -90,7 +104,6 @@ class PlayingQueueAdapter( holder.image?.alpha = alpha holder.title?.alpha = alpha holder.text?.alpha = alpha - holder.imageText?.alpha = alpha holder.paletteColorContainer?.alpha = alpha holder.dragView?.alpha = alpha holder.menu?.alpha = alpha @@ -143,7 +156,6 @@ class PlayingQueueAdapter( } init { - imageText?.visibility = View.VISIBLE dragView?.visibility = View.VISIBLE } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt index 8c2f642e6..9960c3232 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt @@ -177,7 +177,6 @@ open class SongAdapter( get() = dataSet[layoutPosition] init { - setImageTransitionName(activity.getString(R.string.transition_album_art)) menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) { override val song: Song get() = this@ViewHolder.song diff --git a/app/src/main/res/layout-sw600dp/item_queue.xml b/app/src/main/res/layout-sw600dp/item_queue.xml index 20c4531ec..6b03e7a47 100644 --- a/app/src/main/res/layout-sw600dp/item_queue.xml +++ b/app/src/main/res/layout-sw600dp/item_queue.xml @@ -51,6 +51,16 @@ app:layout_constraintStart_toEndOf="@id/drag_view" app:layout_constraintTop_toTopOf="parent"> + + + Date: Fri, 9 Oct 2020 23:37:08 +0530 Subject: [PATCH 50/72] Bump up the build --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0b3ba9096..854e1a74e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10445 - versionName '3.6.100' + "_" + getDate() + versionCode 10448 + versionName '3.6.300' + "_" + getDate() multiDexEnabled true From 2432080d3cdcd399f3951cd9e744df1cbcb494e6 Mon Sep 17 00:00:00 2001 From: "Daksh P. Jain" Date: Sun, 11 Oct 2020 10:15:15 +0530 Subject: [PATCH 51/72] Update contributors.json --- app/src/main/assets/contributors.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json index 217d718a5..e085d3797 100644 --- a/app/src/main/assets/contributors.json +++ b/app/src/main/assets/contributors.json @@ -7,7 +7,7 @@ }, { "name": "Lennart Glamann", - "summary": "Play Store banner and Images", + "summary": "Play Store Banner & Images", "link": "https://t.me/FlixbusLennart", "image": "https://i.imgur.com/Q5Nsx1R.jpg" }, @@ -22,5 +22,11 @@ "summary": "Support Representative & Moderator", "link": "https://t.me/MilindGoel15", "image": "https://i.imgur.com/Bz4De21_d.jpg" + }, +{ + "name": "Haythem Gataa", + "summary": "App Logo Designer", + "link": "https://dribbble.com/haythemgataa", + "image": "https://i.imgur.com/g5RuIZq.jpg" } ] From a0f439409981fb1c29a06ccd62faf78b5c928e02 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 22:45:27 +0530 Subject: [PATCH 52/72] Added new icon Fix bottom tabs showing rotating, coming notification or widgets --- app/src/main/ic_launcher-playstore.png | Bin 14330 -> 18260 bytes .../retromusic/activities/MainActivity.kt | 3 +- .../base/AbsSlidingMusicPanelActivity.kt | 99 +++---- .../adapter/playlist/PlaylistAdapter.kt | 59 ++-- .../retromusic/appwidgets/AppWidgetBig.kt | 8 +- .../retromusic/appwidgets/AppWidgetCard.kt | 7 +- .../retromusic/appwidgets/AppWidgetClassic.kt | 7 +- .../retromusic/appwidgets/AppWidgetSmall.kt | 7 +- .../retromusic/appwidgets/AppWidgetText.kt | 7 +- .../retromusic/extensions/ActivityEx.kt | 5 + .../fragments/DetailListFragment.kt | 2 +- .../retromusic/fragments/LibraryViewModel.kt | 5 - .../fragments/albums/AlbumDetailsFragment.kt | 15 +- .../artists/ArtistDetailsFragment.kt | 14 +- .../fragments/base/AbsRecyclerViewFragment.kt | 3 +- .../fragments/folder/FoldersFragment.java | 3 +- .../fragments/genres/GenreDetailsFragment.kt | 2 +- .../retromusic/fragments/home/HomeFragment.kt | 3 +- .../fragments/library/LibraryFragment.kt | 4 +- .../playlists/PlaylistDetailsFragment.kt | 4 +- .../fragments/playlists/PlaylistsFragment.kt | 2 +- .../fragments/search/SearchFragment.kt | 8 +- .../notification/PlayingNotificationImpl.kt | 2 +- .../notification/PlayingNotificationOreo.kt | 2 +- .../util/AutoGeneratedPlaylistBitmap.java | 272 +++++++++--------- .../main/res/drawable-xxxhdpi/ic_splash.png | Bin 35126 -> 12202 bytes .../main/res/drawable/ic_retro_music_icon.xml | 67 ----- .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3323 -> 1838 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2370 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3323 -> 3821 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2204 -> 1286 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1560 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2204 -> 2395 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4613 -> 2591 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3257 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 4613 -> 5414 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7157 -> 4002 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 5083 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 7157 -> 8484 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10135 -> 5597 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 7072 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 10135 -> 12231 bytes .../res/values/ic_launcher_background.xml | 2 +- 45 files changed, 272 insertions(+), 344 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_retro_music_icon.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 7730a4738f55a5fe35ff37680595e35a3e07d6d6..c632d3782458723edbbc2e73bcde88cb431b38a3 100644 GIT binary patch literal 18260 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 literal 14330 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 - val isQueueEmpty = MusicPlayerRemote.playingQueue.isEmpty() - when (state) { - EXPAND -> { - println("EXPAND") - expandPanel() - bottomNavigationView.translateXAnimate(150f) - } - HIDE -> { - println("HIDE") + fun setBottomBarVisibility(visible: Int) { + bottomNavigationView.visibility = visible + hideBottomBar(MusicPlayerRemote.playingQueue.isEmpty()) + } + + private fun hideBottomBar(hide: Boolean) { + val heightOfBar = dip(R.dimen.mini_player_height) + val heightOfBarWithTabs = dip(R.dimen.mini_player_height_expanded) + val isVisible = bottomNavigationView.isVisible + if (hide) { + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.peekHeight = 0 + ViewCompat.setElevation(slidingPanel, 0f) + ViewCompat.setElevation(bottomNavigationView, 10f) + collapsePanel() + } else { + if (MusicPlayerRemote.playingQueue.isNotEmpty()) { + bottomSheetBehavior.isHideable = false + if (isVisible) { + bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs) bottomNavigationView.translateXAnimate(0f) - bottomSheetBehavior.isHideable = true - bottomSheetBehavior.peekHeightAnimate(0) - bottomSheetBehavior.state = STATE_COLLAPSED - ViewCompat.setElevation(slidingPanel, 0f) - ViewCompat.setElevation(bottomNavigationView, 10f) - } - COLLAPSED_WITH -> { - println("COLLAPSED_WITH") - val heightOfBar = bottomNavigationView.height - val height = if (isQueueEmpty) 0 else (heightOfBar * 2) - 24 - ViewCompat.setElevation(bottomNavigationView, 20f) - ViewCompat.setElevation(slidingPanel, 20f) - bottomSheetBehavior.isHideable = false - bottomSheetBehavior.peekHeightAnimate(height) - bottomNavigationView.translateXAnimate(0f) - } - COLLAPSED_WITHOUT -> { - println("COLLAPSED_WITHOUT") - val heightOfBar = bottomNavigationView.height - val height = if (isQueueEmpty) 0 else heightOfBar - 24 - ViewCompat.setElevation(bottomNavigationView, 10f) - ViewCompat.setElevation(slidingPanel, 10f) - bottomSheetBehavior.isHideable = false - bottomSheetBehavior.peekHeightAnimate(height) - bottomNavigationView.translateXAnimate(150f) - } - else -> { - println("else") - bottomSheetBehavior.isHideable = true - bottomSheetBehavior.peekHeight = 0 - collapsePanel() - ViewCompat.setElevation(slidingPanel, 0f) - ViewCompat.setElevation(bottomNavigationView, 10f) + } else { + bottomSheetBehavior.peekHeightAnimate(heightOfBar) + bottomNavigationView.translateXAnimate(500f) } + ViewCompat.setElevation(slidingPanel, 10f) + ViewCompat.setElevation(bottomNavigationView, 10f) } - }) + } } private fun chooseFragmentForTheme() { diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt index df14d37d1..6a941af35 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt @@ -14,10 +14,8 @@ */ package code.name.monkey.retromusic.adapter.playlist -import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.Drawable -import android.os.AsyncTask import android.text.TextUtils import android.view.LayoutInflater import android.view.MenuItem @@ -26,6 +24,7 @@ import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu import androidx.core.view.ViewCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.TintHelper import code.name.monkey.retromusic.R @@ -41,16 +40,17 @@ import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.helper.menu.SongsMenuHelper import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.interfaces.IPlaylistClickListener -import code.name.monkey.retromusic.model.Playlist import code.name.monkey.retromusic.model.Song -import code.name.monkey.retromusic.repository.PlaylistSongsLoader import code.name.monkey.retromusic.util.AutoGeneratedPlaylistBitmap import code.name.monkey.retromusic.util.MusicUtil -import code.name.monkey.retromusic.util.RetroColorUtil +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PlaylistAdapter( private val activity: FragmentActivity, - var dataSet: List, + private var dataSet: List, private var itemLayoutRes: Int, ICabHolder: ICabHolder?, private val listener: IPlaylistClickListener @@ -102,7 +102,7 @@ class PlaylistAdapter( } else { holder.menu?.show() } - // PlaylistBitmapLoader(this, holder, playlist).execute() + //playlistBitmapLoader(activity, holder, playlist) } private fun getIconRes(): Drawable = TintHelper.createTintedDrawable( @@ -183,28 +183,35 @@ class PlaylistAdapter( } } - class PlaylistBitmapLoader( - private var adapter: PlaylistAdapter, - private var viewHolder: ViewHolder, - private var playlist: Playlist - ) : AsyncTask() { + private fun playlistBitmapLoader( + activity: FragmentActivity, + viewHolder: ViewHolder, + playlist: PlaylistWithSongs + ) { - override fun doInBackground(vararg params: Void?): Bitmap { - val songs = PlaylistSongsLoader.getPlaylistSongList(adapter.activity, playlist) - return AutoGeneratedPlaylistBitmap.getBitmap(adapter.activity, songs, false, false) + activity.lifecycleScope.launch(IO) { + val songs = playlist.songs.toSongs() + val bitmap = AutoGeneratedPlaylistBitmap.getBitmap(activity, songs, false, false) + withContext(Main) { viewHolder.image?.setImageBitmap(bitmap) } } - override fun onPostExecute(result: Bitmap?) { - super.onPostExecute(result) - viewHolder.image?.setImageBitmap(result) - val color = RetroColorUtil.getColor( - RetroColorUtil.generatePalette( - result - ), - ATHUtil.resolveColor(adapter.activity, R.attr.colorSurface) - ) - viewHolder.paletteColorContainer?.setBackgroundColor(color) - } + /* + override fun doInBackground(vararg params: Void?): Bitmap { + val songs = playlist.songs.toSongs() + return AutoGeneratedPlaylistBitmap.getBitmap(activity, songs, false, false) + } + + override fun onPostExecute(result: Bitmap?) { + super.onPostExecute(result) + viewHolder.image?.setImageBitmap(result) + val color = RetroColorUtil.getColor( + RetroColorUtil.generatePalette( + result + ), + ATHUtil.resolveColor(activity, R.attr.colorSurface) + ) + viewHolder.paletteColorContainer?.setBackgroundColor(color) + }*/ } companion object { diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt index 6bad7c4c9..c3aee8205 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt @@ -30,6 +30,7 @@ import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService.* +import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.bumptech.glide.request.animation.GlideAnimation @@ -193,8 +194,11 @@ class AppWidgetBig : BaseAppWidget() { * Link up various button actions using [PendingIntent]. */ private fun linkButtons(context: Context, views: RemoteViews) { - val action = - Intent(context, MainActivity::class.java).putExtra(MainActivity.EXPAND_PANEL, true) + val action = Intent(context, MainActivity::class.java) + .putExtra( + MainActivity.EXPAND_PANEL, + PreferenceUtil.isExpandPanel + ) var pendingIntent: PendingIntent val serviceName = ComponentName(context, MusicService::class.java) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt index 7e756d6d8..8c6935c04 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt @@ -32,6 +32,7 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.ImageUtil +import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.bumptech.glide.request.animation.GlideAnimation @@ -217,7 +218,11 @@ class AppWidgetCard : BaseAppWidget() { * Link up various button actions using [PendingIntent]. */ private fun linkButtons(context: Context, views: RemoteViews) { - val action: Intent = Intent(context, MainActivity::class.java).putExtra("expand", true) + val action = Intent(context, MainActivity::class.java) + .putExtra( + MainActivity.EXPAND_PANEL, + PreferenceUtil.isExpandPanel + ) var pendingIntent: PendingIntent val serviceName = ComponentName(context, MusicService::class.java) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt index b57cc91c8..63291abf8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt @@ -33,6 +33,7 @@ import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.ImageUtil +import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.bumptech.glide.request.animation.GlideAnimation @@ -207,7 +208,11 @@ class AppWidgetClassic : BaseAppWidget() { * Link up various button actions using [PendingIntent]. */ private fun linkButtons(context: Context, views: RemoteViews) { - val action = Intent(context, MainActivity::class.java).putExtra("expand", true) + val action = Intent(context, MainActivity::class.java) + .putExtra( + MainActivity.EXPAND_PANEL, + PreferenceUtil.isExpandPanel + ) var pendingIntent: PendingIntent val serviceName = ComponentName(context, MusicService::class.java) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt index f09b48491..e46d69e65 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt @@ -31,6 +31,7 @@ import code.name.monkey.retromusic.glide.SongGlideRequest import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService.* +import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.bumptech.glide.request.animation.GlideAnimation @@ -192,7 +193,11 @@ class AppWidgetSmall : BaseAppWidget() { * Link up various button actions using [PendingIntent]. */ private fun linkButtons(context: Context, views: RemoteViews) { - val action = Intent(context, MainActivity::class.java).putExtra("expand", true) + val action = Intent(context, MainActivity::class.java) + .putExtra( + MainActivity.EXPAND_PANEL, + PreferenceUtil.isExpandPanel + ) var pendingIntent: PendingIntent val serviceName = ComponentName(context, MusicService::class.java) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt index a09e3ff2d..961717d11 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt @@ -28,6 +28,7 @@ import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService.* +import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil class AppWidgetText : BaseAppWidget() { @@ -77,7 +78,11 @@ class AppWidgetText : BaseAppWidget() { * Link up various button actions using [PendingIntent]. */ private fun linkButtons(context: Context, views: RemoteViews) { - val action = Intent(context, MainActivity::class.java).putExtra("expand", true) + val action = Intent(context, MainActivity::class.java) + .putExtra( + MainActivity.EXPAND_PANEL, + PreferenceUtil.isExpandPanel + ) var pendingIntent: PendingIntent val serviceName = ComponentName(context, MusicService::class.java) diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt index 718e5a9fb..991765898 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ActivityEx.kt @@ -15,6 +15,7 @@ package code.name.monkey.retromusic.extensions import android.app.Activity +import androidx.annotation.DimenRes import androidx.appcompat.app.AppCompatActivity import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import com.google.android.material.appbar.MaterialToolbar @@ -34,3 +35,7 @@ inline fun Activity.extraNotNull(key: String, default: T? = nu val value = intent?.extras?.get(key) requireNotNull(if (value is T) value else default) { key } } + +fun Activity.dip(@DimenRes id: Int): Int { + return resources.getDimensionPixelSize(id) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt index 7037909c9..59267957a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/DetailListFragment.kt @@ -44,7 +44,7 @@ class DetailListFragment : AbsMainActivityFragment(R.layout.fragment_playlist_de override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + mainActivity.setBottomBarVisibility(View.GONE) mainActivity.setSupportActionBar(toolbar) progressIndicator.hide() when (args.type) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt index 96b28733e..b0cb2d55f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/LibraryViewModel.kt @@ -60,11 +60,6 @@ class LibraryViewModel( private val genres = MutableLiveData>() private val searchResults = MutableLiveData>() val paletteColor: LiveData = _paletteColor - val panelState: MutableLiveData = MutableLiveData() - - fun setPanelState(state: NowPlayingPanelState) { - panelState.postValue(state) - } init { loadLibraryContent() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt index e01f02fbf..f4008bbe2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/albums/AlbumDetailsFragment.kt @@ -17,7 +17,11 @@ package code.name.monkey.retromusic.fragments.albums import android.app.ActivityOptions import android.content.Intent import android.os.Bundle -import android.view.* +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.SubMenu +import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.os.bundleOf import androidx.core.text.HtmlCompat @@ -59,14 +63,12 @@ import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmAlbum import code.name.monkey.retromusic.repository.RealRepository -import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import com.bumptech.glide.Glide import com.google.android.material.transition.MaterialContainerTransform -import java.util.* import kotlinx.android.synthetic.main.fragment_album_content.* import kotlinx.android.synthetic.main.fragment_album_details.* import kotlinx.coroutines.Dispatchers @@ -75,6 +77,7 @@ import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import java.util.* class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_details), IAlbumClickListener { @@ -90,11 +93,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det private val savedSortOrder: String get() = PreferenceUtil.albumDetailSongSortOrder - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) - } - private fun setUpTransitions() { val transform = MaterialContainerTransform() transform.setAllContainerColors(ATHUtil.resolveColor(requireContext(), R.attr.colorSurface)) @@ -109,6 +107,7 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) + mainActivity.setBottomBarVisibility(View.GONE) mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) toolbar.title = " " diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt index c9f12a702..06ddacafd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/artists/ArtistDetailsFragment.kt @@ -51,14 +51,11 @@ import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.network.Result import code.name.monkey.retromusic.network.model.LastFmArtist import code.name.monkey.retromusic.repository.RealRepository -import code.name.monkey.retromusic.state.NowPlayingPanelState import code.name.monkey.retromusic.util.CustomArtistImageUtil import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.Glide import com.google.android.material.transition.MaterialContainerTransform -import java.util.* -import kotlin.collections.ArrayList import kotlinx.android.synthetic.main.fragment_artist_content.* import kotlinx.android.synthetic.main.fragment_artist_details.* import kotlinx.coroutines.Dispatchers @@ -67,6 +64,8 @@ import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import java.util.* +import kotlin.collections.ArrayList class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_details), IAlbumClickListener { @@ -91,22 +90,21 @@ class ArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_artist_d super.onCreate(savedInstanceState) setUpTransitions() } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) + mainActivity.setBottomBarVisibility(View.GONE) + mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) - toolbar.title = null - - setupRecyclerView() - ViewCompat.setTransitionName(container, "artist") postponeEnterTransition() detailsViewModel.getArtist().observe(viewLifecycleOwner, Observer { startPostponedEnterTransition() showArtist(it) }) + setupRecyclerView() playAction.apply { setOnClickListener { MusicPlayerRemote.openQueue(artist.songs, 0, true) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt index 86243a13c..df9518ed2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsRecyclerViewFragment.kt @@ -61,8 +61,7 @@ abstract class AbsRecyclerViewFragment, LM : Recycle override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - println("AbsRecyclerViewFragment") - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) + mainActivity.setBottomBarVisibility(View.VISIBLE) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null initLayoutManager() diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index 5e03ad767..9a839da69 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -75,7 +75,6 @@ import code.name.monkey.retromusic.misc.DialogAsyncTask; import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; import code.name.monkey.retromusic.misc.WrappedAsyncTaskLoader; import code.name.monkey.retromusic.model.Song; -import code.name.monkey.retromusic.state.NowPlayingPanelState; import code.name.monkey.retromusic.util.DensityUtil; import code.name.monkey.retromusic.util.FileUtil; import code.name.monkey.retromusic.util.PreferenceUtil; @@ -166,7 +165,7 @@ public class FoldersFragment extends AbsMainActivityFragment @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { getMainActivity().addMusicServiceEventListener(getLibraryViewModel()); - getLibraryViewModel().setPanelState(NowPlayingPanelState.COLLAPSED_WITH); + getMainActivity().setBottomBarVisibility(View.VISIBLE); getMainActivity().setSupportActionBar(toolbar); getMainActivity().getSupportActionBar().setTitle(null); setStatusBarColorAuto(view); diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt index c2190eb59..2f1b70bf1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/genres/GenreDetailsFragment.kt @@ -47,9 +47,9 @@ class GenreDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playlist_ override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) + mainActivity.setBottomBarVisibility(View.GONE) mainActivity.addMusicServiceEventListener(detailsViewModel) mainActivity.setSupportActionBar(toolbar) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) progressIndicator.hide() setupRecyclerView() detailsViewModel.getSongs().observe(viewLifecycleOwner, androidx.lifecycle.Observer { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt index 520d0836a..6552f06ea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/home/HomeFragment.kt @@ -52,8 +52,7 @@ class HomeFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - println("AbsMainActivityFragment") - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) + mainActivity.setBottomBarVisibility(View.VISIBLE) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null setStatusBarColorAuto(view) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt index 9151ee5a1..a65d81522 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/library/LibraryFragment.kt @@ -18,6 +18,7 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import android.view.View import androidx.core.text.HtmlCompat import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController @@ -40,8 +41,7 @@ class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) - retainInstance = true - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITH) + mainActivity.setBottomBarVisibility(View.VISIBLE) mainActivity.setSupportActionBar(toolbar) mainActivity.supportActionBar?.title = null toolbar.setNavigationOnClickListener { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt index 35d4f2926..d74a1aaf8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt @@ -48,14 +48,12 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + mainActivity.setBottomBarVisibility(View.GONE) mainActivity.addMusicServiceEventListener(viewModel) mainActivity.setSupportActionBar(toolbar) - ViewCompat.setTransitionName(container, "playlist") playlist = arguments.extraPlaylist toolbar.title = playlist.playlistEntity.playlistName - setUpRecyclerView() viewModel.getSongs().observe(viewLifecycleOwner, { songs(it.toSongs()) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt index cc5ef0fc2..8cde24ba8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistsFragment.kt @@ -61,7 +61,7 @@ class PlaylistsFragment : return PlaylistAdapter( requireActivity(), ArrayList(), - R.layout.item_list, + itemLayoutRes(), null, this ) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt index 3c15b136d..d767d8591 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/search/SearchFragment.kt @@ -22,7 +22,6 @@ import android.text.Editable import android.text.TextWatcher import android.view.View import android.view.inputmethod.InputMethodManager -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat.getSystemService import androidx.core.view.isGone import androidx.core.view.isVisible @@ -35,11 +34,10 @@ import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.dipToPix import code.name.monkey.retromusic.extensions.showToast import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment -import code.name.monkey.retromusic.state.NowPlayingPanelState import com.google.android.material.textfield.TextInputEditText +import kotlinx.android.synthetic.main.fragment_search.* import java.util.* import kotlin.collections.ArrayList -import kotlinx.android.synthetic.main.fragment_search.* class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWatcher { companion object { @@ -52,7 +50,7 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - libraryViewModel.setPanelState(NowPlayingPanelState.COLLAPSED_WITHOUT) + mainActivity.setBottomBarVisibility(View.GONE) mainActivity.setSupportActionBar(toolbar) libraryViewModel.clearSearchResult() setupRecyclerView() @@ -86,7 +84,7 @@ class SearchFragment : AbsMainActivityFragment(R.layout.fragment_search), TextWa } private fun setupRecyclerView() { - searchAdapter = SearchAdapter(requireActivity() as AppCompatActivity, emptyList()) + searchAdapter = SearchAdapter(requireActivity(), emptyList()) searchAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { super.onChanged() diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt index c7385fd89..e7a2ca717 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt @@ -55,7 +55,7 @@ class PlayingNotificationImpl : PlayingNotification() { if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border val action = Intent(service, MainActivity::class.java) - action.putExtra(MainActivity.EXPAND_PANEL, true) + action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel) action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP val clickIntent = PendingIntent.getActivity(service, 0, action, PendingIntent.FLAG_UPDATE_CURRENT) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt index 14243faa8..7612c7a03 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt @@ -73,7 +73,7 @@ class PlayingNotificationOreo : PlayingNotification() { val notificationLayoutBig = getCombinedRemoteViews(false, song) val action = Intent(service, MainActivity::class.java) - action.putExtra(MainActivity.EXPAND_PANEL, true) + action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel) action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP val clickIntent = PendingIntent diff --git a/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java b/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java index 026c37199..f219ee391 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/AutoGeneratedPlaylistBitmap.java @@ -8,43 +8,47 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; + import androidx.annotation.NonNull; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.model.Song; + import com.bumptech.glide.Glide; + import java.util.ArrayList; import java.util.List; +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.model.Song; + public class AutoGeneratedPlaylistBitmap { - private static final String TAG = "AutoGeneratedPB"; + private static final String TAG = "AutoGeneratedPB"; - /* - public static Bitmap getBitmapWithCollectionFrame(Context context, List songPlaylist, boolean round, boolean blur) { - Bitmap bitmap = getBitmap(context,songPlaylist,round,blur); - int w = bitmap.getWidth(); - Bitmap ret = Bitmap.createBitmap(w,w,Bitmap.Config.ARGB_8888); - } - */ - public static Bitmap getBitmap( - Context context, List songPlaylist, boolean round, boolean blur) { - if (songPlaylist == null) return null; - long start = System.currentTimeMillis(); - // lấy toàn bộ album id, loại bỏ trùng nhau - List albumID = new ArrayList<>(); - for (Song song : songPlaylist) { - if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId()); + /* + public static Bitmap getBitmapWithCollectionFrame(Context context, List songPlaylist, boolean round, boolean blur) { + Bitmap bitmap = getBitmap(context,songPlaylist,round,blur); + int w = bitmap.getWidth(); + Bitmap ret = Bitmap.createBitmap(w,w,Bitmap.Config.ARGB_8888); } + */ + public static Bitmap getBitmap( + Context context, List songPlaylist, boolean round, boolean blur) { + if (songPlaylist == null || songPlaylist.isEmpty()) return null; + long start = System.currentTimeMillis(); + // lấy toàn bộ album id, loại bỏ trùng nhau + List albumID = new ArrayList<>(); + for (Song song : songPlaylist) { + if (!albumID.contains(song.getAlbumId())) albumID.add(song.getAlbumId()); + } - long start2 = System.currentTimeMillis() - start; + long start2 = System.currentTimeMillis() - start; - // lấy toàn bộ art tồn tại - List art = new ArrayList(); - for (Long id : albumID) { - Bitmap bitmap = getBitmapWithAlbumId(context, id); - if (bitmap != null) art.add(bitmap); - if (art.size() == 6) break; - } - return MergedImageUtils.INSTANCE.joinImages(art); + // lấy toàn bộ art tồn tại + List art = new ArrayList(); + for (Long id : albumID) { + Bitmap bitmap = getBitmapWithAlbumId(context, id); + if (bitmap != null) art.add(bitmap); + if (art.size() == 6) break; + } + return MergedImageUtils.INSTANCE.joinImages(art); /* long start3 = System.currentTimeMillis() - start2 - start; @@ -70,119 +74,119 @@ public class AutoGeneratedPlaylistBitmap { Log.d(TAG, "getBitmap: time = " + (System.currentTimeMillis() - start) + ", start2 = " + start2 + ", start3 = " + start3); return ret;*/ - } + } - private static Bitmap getBitmapCollection(ArrayList art, boolean round) { - long start = System.currentTimeMillis(); - // lấy kích thước là kích thước của bitmap lớn nhất - int max_width = art.get(0).getWidth(); - for (Bitmap b : art) if (max_width < b.getWidth()) max_width = b.getWidth(); - Bitmap bitmap = Bitmap.createBitmap(max_width, max_width, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setAntiAlias(false); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(max_width / 100); - paint.setColor(0xffffffff); - switch (art.size()) { - case 2: - canvas.drawBitmap(art.get(1), null, new Rect(0, 0, max_width, max_width), null); - canvas.drawBitmap( - art.get(0), null, new Rect(-max_width / 2, 0, max_width / 2, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - break; - case 3: - canvas.drawBitmap( - art.get(0), null, new Rect(-max_width / 4, 0, 3 * max_width / 4, max_width), null); - canvas.drawBitmap( - art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); - canvas.drawBitmap( - art.get(2), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - canvas.drawLine(max_width / 2, max_width / 2, max_width, max_width / 2, paint); - break; - case 4: - canvas.drawBitmap(art.get(0), null, new Rect(0, 0, max_width / 2, max_width / 2), null); - canvas.drawBitmap( - art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); - canvas.drawBitmap( - art.get(2), null, new Rect(0, max_width / 2, max_width / 2, max_width), null); - canvas.drawBitmap( - art.get(3), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); - canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); - canvas.drawLine(0, max_width / 2, max_width, max_width / 2, paint); - break; - // default: canvas.drawBitmap(art.get(0),null,new Rect(0,0,max_width,max_width),null); - default: - - // độ rộng của des bitmap - float w = (float) (Math.sqrt(2) / 2 * max_width); - float b = (float) (max_width / Math.sqrt(5)); - // khoảng cách định nghĩa, dùng để tính vị trí tâm của 4 bức hình xung quanh - float d = (float) (max_width * (0.5f - 1 / Math.sqrt(10))); - float deg = 45; - - for (int i = 0; i < 5; i++) { - canvas.save(); - switch (i) { - case 0: - canvas.translate(max_width / 2, max_width / 2); - canvas.rotate(deg); - // b = (float) (max_width*Math.sqrt(2/5f)); - canvas.drawBitmap(art.get(0), null, new RectF(-b / 2, -b / 2, b / 2, b / 2), null); - break; - case 1: - canvas.translate(d, 0); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(w / 2, -w / 2, w / 2, w / 2, paint); - break; + private static Bitmap getBitmapCollection(ArrayList art, boolean round) { + long start = System.currentTimeMillis(); + // lấy kích thước là kích thước của bitmap lớn nhất + int max_width = art.get(0).getWidth(); + for (Bitmap b : art) if (max_width < b.getWidth()) max_width = b.getWidth(); + Bitmap bitmap = Bitmap.createBitmap(max_width, max_width, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(false); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(max_width / 100); + paint.setColor(0xffffffff); + switch (art.size()) { case 2: - canvas.translate(max_width, d); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, w / 2, w / 2, w / 2, paint); - break; + canvas.drawBitmap(art.get(1), null, new Rect(0, 0, max_width, max_width), null); + canvas.drawBitmap( + art.get(0), null, new Rect(-max_width / 2, 0, max_width / 2, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + break; case 3: - canvas.translate(max_width - d, max_width); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, -w / 2, -w / 2, w / 2, paint); - break; + canvas.drawBitmap( + art.get(0), null, new Rect(-max_width / 4, 0, 3 * max_width / 4, max_width), null); + canvas.drawBitmap( + art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); + canvas.drawBitmap( + art.get(2), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + canvas.drawLine(max_width / 2, max_width / 2, max_width, max_width / 2, paint); + break; case 4: - canvas.translate(0, max_width - d); - canvas.rotate(deg); - canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); - paint.setAntiAlias(true); - canvas.drawLine(-w / 2, -w / 2, w / 2, -w / 2, paint); - break; - } - canvas.restore(); + canvas.drawBitmap(art.get(0), null, new Rect(0, 0, max_width / 2, max_width / 2), null); + canvas.drawBitmap( + art.get(1), null, new Rect(max_width / 2, 0, max_width, max_width / 2), null); + canvas.drawBitmap( + art.get(2), null, new Rect(0, max_width / 2, max_width / 2, max_width), null); + canvas.drawBitmap( + art.get(3), null, new Rect(max_width / 2, max_width / 2, max_width, max_width), null); + canvas.drawLine(max_width / 2, 0, max_width / 2, max_width, paint); + canvas.drawLine(0, max_width / 2, max_width, max_width / 2, paint); + break; + // default: canvas.drawBitmap(art.get(0),null,new Rect(0,0,max_width,max_width),null); + default: + + // độ rộng của des bitmap + float w = (float) (Math.sqrt(2) / 2 * max_width); + float b = (float) (max_width / Math.sqrt(5)); + // khoảng cách định nghĩa, dùng để tính vị trí tâm của 4 bức hình xung quanh + float d = (float) (max_width * (0.5f - 1 / Math.sqrt(10))); + float deg = 45; + + for (int i = 0; i < 5; i++) { + canvas.save(); + switch (i) { + case 0: + canvas.translate(max_width / 2, max_width / 2); + canvas.rotate(deg); + // b = (float) (max_width*Math.sqrt(2/5f)); + canvas.drawBitmap(art.get(0), null, new RectF(-b / 2, -b / 2, b / 2, b / 2), null); + break; + case 1: + canvas.translate(d, 0); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(w / 2, -w / 2, w / 2, w / 2, paint); + break; + case 2: + canvas.translate(max_width, d); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, w / 2, w / 2, w / 2, paint); + break; + case 3: + canvas.translate(max_width - d, max_width); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, -w / 2, -w / 2, w / 2, paint); + break; + case 4: + canvas.translate(0, max_width - d); + canvas.rotate(deg); + canvas.drawBitmap(art.get(i), null, new RectF(-w / 2, -w / 2, w / 2, w / 2), null); + paint.setAntiAlias(true); + canvas.drawLine(-w / 2, -w / 2, w / 2, -w / 2, paint); + break; + } + canvas.restore(); + } + } + Log.d(TAG, "getBitmapCollection: smalltime = " + (System.currentTimeMillis() - start)); + if (round) return BitmapEditor.getRoundedCornerBitmap(bitmap, bitmap.getWidth() / 40); + else return bitmap; + } + + private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Long id) { + try { + return Glide.with(context) + .load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id)) + .asBitmap() + .into(200, 200) + .get(); + } catch (Exception e) { + return null; } } - Log.d(TAG, "getBitmapCollection: smalltime = " + (System.currentTimeMillis() - start)); - if (round) return BitmapEditor.getRoundedCornerBitmap(bitmap, bitmap.getWidth() / 40); - else return bitmap; - } - private static Bitmap getBitmapWithAlbumId(@NonNull Context context, Long id) { - try { - return Glide.with(context) - .load(MusicUtil.INSTANCE.getMediaStoreAlbumCoverUri(id)) - .asBitmap() - .into(200, 200) - .get(); - } catch (Exception e) { - return null; + public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) { + if (round) + return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); + return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); } - } - - public static Bitmap getDefaultBitmap(@NonNull Context context, boolean round) { - if (round) - return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); - return BitmapFactory.decodeResource(context.getResources(), R.drawable.default_album_art); - } } diff --git a/app/src/main/res/drawable-xxxhdpi/ic_splash.png b/app/src/main/res/drawable-xxxhdpi/ic_splash.png index 07a310c4c4c291a14fa877dc86e647f9ccc240e2..727bc25f2f0d758bac410b4da70fcbdcbc40c74f 100644 GIT binary patch literal 12202 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 XjDN#V`J{_N#e$Q^P8sJPb-4Fmj0Z-s literal 35126 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

m%D`KhDHN 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 - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1f..036d09bc5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1f..036d09bc5 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index ad35070726c01bd3486444e3c6bead58daa23516..a30acf391ff85645efd88853abef8f593d9740aa 100644 GIT binary patch delta 1824 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 QL;potAa5e*SMNOh2j4P#Bme*a literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index ad35070726c01bd3486444e3c6bead58daa23516..c1a5978fe22de71a13a4f6b5165b4c8e35bc854c 100644 GIT binary patch literal 3821 zcmVGuJsqKn15XrFs_S&=VX}OYJ3+^9*WX8A(}Y==V@_c+{glhn&+gb9gAq zpprgJDl-N#31mj*;ZDq4)?4eij~y;tIClhl=llNOJsi%k_wTH=_Fnt!b0m@$TmZm= z$x@5B|4aT~7FJ;^lr{b8aNMI~{&K*WB~axowfw&=st3vuHGs*HZELH~IUg4=++Ek$ z)*a=J@8e0PxYoQz9dN0&+)Ol1s zRvbubwOS|f$I&@Z!R(y9G~Cy^wOfp(;O|kWL#PrBDC%0auvIl%>NKQg|Em@3Unc}c z=VsR!rQv>7tu#vaRC)oFS^)_#P(0OI4Onzdx5moU(gE0#tvjK<7lP5e>n2bg zyM_=HmBC4~d@TXTMzH=is)B*3GXofHptx(|Qb}cMtgL2*gTI{-N{GN>YRm|wQSoJ9 zaql9P(X5aJaE7DOsUWOS>XwG15vO=i=~Y!#Bg_Jc4}3K>HB-op1#qA>N>H{+l}h!p zNrN{}A1^waoLmrup*4#yBmRI)Ci66jCGjUj%bEN*!4X0tYsd0KK|#SZqe9|M68)HB zPUYhUM-vj(qcSZm?HQxEo1l*)rC`Tmj%YZ!ImNJlZz#;1wgtwG_Jie%KZmQAV?d=W zZ&65PWn~%h@$p@Sbu?^}p1Kz4Q^xIX)iRye%Kx<}lhAR(xEb9TT_(TQetHER0v^776ySF_T|wa|M0 z&YE32vN;zL3y4>D+c#j1_m_}xC!OoJm>Lr0)#c^okdTltMYOB&K??(Dgs!cY zp@2xzcC+DcBYa@jr)Tl*mY5V2U$@-c+(=OYn>RD0>dJQr&1pgPKw*F%KW+mApSy`c zzuIJ@08~#*L*TdHe*1f~v4}UZ%F4^RWL^ zYxEYaq@<+y`0?WoT04(CIfX*8j9HJ~!x#4o6wrOw6|e~id#LTF!6xWmV6*>ousQSv z>dX4t9|UbSZ-@3C>!Fj~{3bySAF>+0+8c%yQW_N!HcEg^{+n8@i20rcy4QaUaWB&E zWz}FCR|b+8UP_RpTmwnLd5|c>K~jFXp~_IjpseRBTo%l4UuC6iI%`nr?4-5>n6EdmSdSU!==f{-|xg(7N zA}jji7D)1gwOf&ftgau2j)Rso8_4Yu)Rs-(Kt_7LL46`>%_1WsrxS}Ge)z$ut{vyVizC;;f&Jl7T%=dpQcL++ zY;5c%)+e&ov>;0i&|e#4yhkgb7!^no<3N&&u}Bdn%8D#Ux4+&3$5G;&=7pkYI|56-M$X(*i=p%0QBO6(ofhK%$Be1negqFl`)_afMK^3W+= zp`n(p9X((qrY$?SpMkHhZHXr)*Sctv(MV$KfUIL5e?#9?87t@jz`1$$$*6crtLywnP&DP(YcYJf^>kCo zFW;k9eD>LALmB|Oc=6(3Jh)un1XX81q@-U!k{2v0V0V3S4cK*Gz~y+3y%urfO{9Ky zi^0iZDGcbl44mwja@z=oP_)g-ei^(zb1S-lmj-}>gM){d9Z-A$NYW8b zaY&O^WT6NLhk+~pgwkgTI6EwZL71NnarS~I2h`VKr_>v6D5TMVZ6OZ_d;i_=^sqO1KWo4yGrP6No05M1%u_t}QHrEj{e}HyUL7HtFYKz`{@J!D$Q> z0U%gI2YS`lAa9sFej|Jt6oRfJ7u8WrN=o_}SxD>6(PrK1_WAkwCt3y&>P|N1X=p*E z5A!su{7cY(^*_O7#9LYc)rYl)(}*V@YNX3r@bf*y9#GbbPBm`dzI~cl)En|(iCG)W zLpGW;S#3^6hhB80QfNBZjR;cUdhO&KOr;2>SdyJjgE6cSMm3(3zQ;dDEs%8_cr#`ma1NVx9By z@-7$}hca@jV9rNa;rDdcEAJM;b7Qw-9Pw(_iU`cc4aeY*#B75mLh$kD*I$2)Kn7jv zS&v2>>JS|rz1GmC-Vi{f2@9h!IIq=eMbAI;Hq7_j36WP~L8YoNI3!?LQPXJR$&)8P zAO;A^rqMTX@W;l}({pHfd3k{$Ze}PTvZ7Iw4xkmSYyjzHOc%cU_BzOADq}qtDd zJG*Goq)9Zf%0}xOMW;@kYJ)%Bl9Q7S8d)?H5b5PLso>)B4gy-mftfJwL)f+BEM#RC z8vD6eEoKmK?b@~PsqTzHlaHP|U^e>-9i;C7zA*s3>RSq9Up)oSJ>vu3%l{3rF)2`G z3!6c8I zg>}1i>sA!j*^bq{>4g!r4GIeKsI08i%ZdyEL}0wO$>E!D6%>?I8QYy2SPy3JuzB<5 zg;eK#`}T>O`MPAul6Lsh=kDFR!PNZeI)N%Pw^}biue~pa-QOr6DWk&B2Z5rd;j!35 zPgDD_I*VH{P*E(hojZ5#SQ>(4RwTMb%gn0=&yC{2%tA0*4k;imshsQ2n0oRQ-*RCX zQOuYz!=38t@9(eMJViW!i1t2r?wmi(!a_)*V^Kj#4Xoa&5C!I~T?z=rw4_8{WtLg5 z1fS|6lgZ%l;ltagj_cN~Yp>5lSBo)Y#?a#r=lJ;ea56_SOEHD0s#e2S=gYZxq79BG zy5R9JCFB%Tnr%8TgTuMj_3PKKV%-L?I_mc(H8Z=e_V)Hqr=_K(=<>>wOj!$`98hs_ zMF{2%%)0`=RYFQu1%fp1JZUOBg^N3P?xfn;*^Q*Sd3kvm_DTYtpacH&TD*Aibj;Hf z<{vufoT!^ZGkM=3y?LY;5dRzrRCscs4tlBJ-G&P%aDtl^bsuHt>nN%lMH^Z0t z^XE(P#~~mfU|C^dp;{M6Li08^6iqmoPN=a%=#6!8WOXvyM+z)v&6?E(fBOdp2KwaZ z=d0Ny&zhF1v<^3=hKGYUcpcSY`t<2tjR}WeEXR%=ODA^5*t8-mD@)EY;Z_NW56BdA z(5)03Hf$ivaUL~l6b;Cm>_uP82@@vJmk*p4ELbq(-o1ONthcQ;^ht=Z`#Dr5n(q@A z7nkbc;qe-knU&q-Z$Vp5nKFg$LO-cg`g~MWR77cMDF|Gyt_8u+wKY_?a#EhbK!NN*F{qlOzkdB_(&J!vclRlmFJBHRC@A1MehgHtDP52MbOmcw>8U6^mAL5U=0-2@ z4yLkE8CwF5aItoEbtT{Ki+T#X?MrXmxN)6YI*LzhdGtjILoADqW#=F$YTjb#T69gk z_Hrr%m4%h5)qL!PvnD4eC;A$TGs?x=+k3&KOP5Ziq@?80;}qVi*rzLMh2J>A|EXEU zA4#x{+n0{vkEe4f6bhho(=}GETuH+fG-bt^%Fr6DM%!Zb!V51*ncESlr*Lr(3JUu8 z+O=!p$;rvtGRW z=;tIRCMMB77~9SqJ$iH}9ZTn6)CT|1lUX7I6OuAYR5BZDbvq00000NkvXXu0mjfaX(W% delta 3320 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 delta 2193 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index a5786f83ca909c76986fd3841e82b915e2feb86f..093fff061b5c6569461cc28a66451002ba045c1f 100644 GIT binary patch delta 2385 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e3cb18319c851d35920250773082c8d65e6c4aec GIT binary patch literal 3257 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 7b5aa2c9c17fb0976eb89b03ab85d2fe0328d6bd..ff9625ba1d32c65067cefa38c19266c692cf5c72 100644 GIT binary patch literal 5414 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 Qi2wiq07*qoM6N<$f<;whg8%>k literal 4613 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 3523604e7c16dab1fbdf0b286c1ee868ecb66bd1..5d629bb1ebabb00b66a064f864b6a5ce1eee6575 100644 GIT binary patch literal 4002 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 W(WTI1qxx&tOZ-0xtrblG literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 3523604e7c16dab1fbdf0b286c1ee868ecb66bd1..710a8ff5deeda48a07fbf4e29e8f0b6ebffed309 100644 GIT binary patch literal 8484 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 PQ-*(={6+W^8S(!BxI0z2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index a57669d6581321ec62e14476a520095a247284e8..8ed2b6c2c34343fdf4af34c8647e477605bb9b15 100644 GIT binary patch literal 12231 zcmYLPWmFpt(@jDM?(SNwxVw8PP@s6x;%>zuxEFYEDM5-Ar%-6IP=dRZ;#!=R;0|Bj zU*DdS-Tc^{-E&6n+&hz?tF4BIO^poz0Pr-_mGw|p`2T*G5Y$<3#T5ep;QOedtY8qZ z^!FWxF{#mS2w0hrh$xFKp2HYUJ?daH+`z`BO5$Qui)C}KrDdZb)TC^iu$$C{FfLUP zz~&M?PK45d-zcldb84Dd9e6$k$4W`o;xsqfLHS!Ei++qR z*EyzMmgFBlG@IG@7_NiANvvERbDSB8nE~joH3tapT;czWJzw6eCWD?o{)CeJKL2Gi zmGcVn!eSHOO-Tqn6&`6vbQls^gsGyUVmmZ>gnRRrJ@Tiz$hRH#pQ84s5#qt2wUrXp zfCpyC0~bk&dM}IB@|FFXJnaSrXg&L5*e(t|`PB{XkTd@w(G3giTWejjLoYth^_}v+ zWR{*m4gl=(CO<>^$^hyV?vS;7V|~fT=YgG}{0l6{<6_(&6nYSd7NU)g zj$RWQ5^~5-Yyf-^%&R4GQkIM1CP2#*u8n60rYpsjmX=Zr5my1iwq)Od%W)TSxw|+O zG!Xu#x3{-qMY5rXBkH;dy*yi@9cjN)bJrTEOqlO*BYd3&5)u+t7m7{LohE+%{Q1n% z0DFU^;2+kv-HHThMmh`+51%v&JDDJ9rh4m`{`iTag^#nL-nSSH?gMM!KQ67E+uok3 z`6_T^A5^1LF>hgcC(u&IOm@!)fxg@lJuvVif6Eg8K^+5wg9MeV{Iyvd?D;(~PiRs^ z&xdrz0$Kq9fs;8dV&q27=+39%BUZ@M(ksvdXU6b9`(7Z~UwP^2)*9{)`3vw=0!6kArJu6~0ax5;-ahA4>aFULdy}yB&VbWW zzZC9b67I?p9rqik@p~OrRnchhdX#1OX~Z?;3HQ^gLuAL1Fw#*7E$G7|wKV%9F!=d9 zfPjJ36KoCrmCQsQ&jm#w5K3ScKT>{{M_+b=Fz>G~!K1-B`}J*nvGK@3e)~CWPe)Pu zu_JuG-_-dZqwDFqp|6ipi3rli0X5%=wrz+h6kT198RE&%kLTij_gFW76#Q^Lt{tN_ zK0WHOnhkJ`fg9O-_&yMK`op4*kIf;bkFlXj-a&k6LO-+ftcny+GV{&zucR!A; z_X}50V5+tB0dpUw)C%6j%&*vkLc|;}q!Yp?;5EA!j$vW4(t&5JV}pa0L~^Fw{el>w zT%n?gO#3o)o+PFVI(j&$w^fOOmRj?%pUrkKhKCPu;m)iIhKA(Q0Z1#U7trp{TfiT; zk+Rlj90L1K51k-N(f0|ay?iaImufyHpGgMmy#$jYjAc#%>588BSAP>S!K!-y$=+30 zS6}^M+qzk^%)tvfumLZaq|Q@1mIkFe!z;xM;e#TJUd%g!J0(K+e*MJrXTVHSgsn5-6$2&s0$IKK41D_B; z`PAe6^)=1_#)S&(l4-`lH_wg4Zy`P}&zn-fx7rZiARac=b#AoLwMQ$qF#veC^N8b_ zNvWu$#GWAe67dR#3gZg!`5T)w+47Vepoec^xgQ7cqEDXOiOVRfhCkmBVzidqVnK(J=mVRhWQvm}^{!IBwYZY|TyPK!(zNbQKbKXXy zqN4J={+nChLpfu-nqunRqVO+k$oKExVF^vw zIoLo=v>l?2`57ETUMT>Uj(%}_Jg~cPXJJ_Ab!1vv8lRKAYwhgX|2%m)9J4y-S6(@X zg>0_1A$_ofo%B9?Vv5D~%0D}+Vs^QC_}(l@90>aYqV- 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 literal 10135 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 - #1C1C1C + #FFFFFF \ No newline at end of file From 87be373a03a19f5187c52db531d9a8037f16780a Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 22:50:56 +0530 Subject: [PATCH 53/72] Bump the version to v4 To make it 4th anniversary --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 854e1a74e..3afd3f0f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ android { applicationId "code.name.monkey.retromusic" versionCode 10448 - versionName '3.6.300' + "_" + getDate() + versionName '4.0.0' + "_" + getDate() multiDexEnabled true From 535b54ce31e48e1d6404dfcf040c6756b337d125 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 22:52:34 +0530 Subject: [PATCH 54/72] Update AbsSlidingMusicPanelActivity.kt --- .../activities/base/AbsSlidingMusicPanelActivity.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index bdc6fd6d3..9ad5fe8c7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -29,12 +29,7 @@ import code.name.monkey.appthemehelper.util.ATHUtil import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.RetroBottomSheetBehavior -import code.name.monkey.retromusic.extensions.dip -import code.name.monkey.retromusic.extensions.hide -import code.name.monkey.retromusic.extensions.peekHeightAnimate -import code.name.monkey.retromusic.extensions.show -import code.name.monkey.retromusic.extensions.translateXAnimate -import code.name.monkey.retromusic.extensions.whichFragment +import code.name.monkey.retromusic.extensions.* import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.MiniPlayerFragment import code.name.monkey.retromusic.fragments.NowPlayingScreen From 677cbbb7120bc344c78e4e063cee74cf9f2d6cc7 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 23:07:15 +0530 Subject: [PATCH 55/72] Update AbsSlidingMusicPanelActivity.kt --- .../activities/base/AbsSlidingMusicPanelActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 9ad5fe8c7..7f6e223f1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -166,7 +166,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { miniPlayerFragment?.view?.alpha = alpha miniPlayerFragment?.view?.visibility = if (alpha == 0f) View.GONE else View.VISIBLE bottomNavigationView.translationY = progress * 500 - // bottomNavigationView.alpha = alpha + bottomNavigationView.alpha = alpha } open fun onPanelCollapsed() { @@ -342,6 +342,8 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { } else { if (MusicPlayerRemote.playingQueue.isNotEmpty()) { bottomSheetBehavior.isHideable = false + ViewCompat.setElevation(slidingPanel, 10f) + ViewCompat.setElevation(bottomNavigationView, 10f) if (isVisible) { bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs) bottomNavigationView.translateXAnimate(0f) @@ -349,8 +351,6 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { bottomSheetBehavior.peekHeightAnimate(heightOfBar) bottomNavigationView.translateXAnimate(500f) } - ViewCompat.setElevation(slidingPanel, 10f) - ViewCompat.setElevation(bottomNavigationView, 10f) } } } From 4a9a4d68a603a5215c138de5b5a9aef1045212af Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 23:43:35 +0530 Subject: [PATCH 56/72] Update item_queue.xml --- app/src/main/res/layout/item_queue.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/layout/item_queue.xml b/app/src/main/res/layout/item_queue.xml index ce7cff8d0..0df8c24b6 100644 --- a/app/src/main/res/layout/item_queue.xml +++ b/app/src/main/res/layout/item_queue.xml @@ -20,8 +20,6 @@ android:layout_marginEnd="1dp" android:layout_marginBottom="1dp" android:background="?rectSelector" - android:clickable="true" - android:focusable="true" android:minHeight="?attr/listPreferredItemHeight" android:orientation="horizontal" tools:ignore="MissingPrefix"> From b2a5f5a2d1b77a1c7066a8ec6d830f4923878c9f Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Sun, 11 Oct 2020 23:54:34 +0530 Subject: [PATCH 57/72] Update item_queue.xml --- app/src/main/res/layout-sw600dp/item_queue.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/layout-sw600dp/item_queue.xml b/app/src/main/res/layout-sw600dp/item_queue.xml index 6b03e7a47..7917c4ea8 100644 --- a/app/src/main/res/layout-sw600dp/item_queue.xml +++ b/app/src/main/res/layout-sw600dp/item_queue.xml @@ -20,8 +20,6 @@ android:layout_marginEnd="1dp" android:layout_marginBottom="1dp" android:background="?rectSelector" - android:clickable="true" - android:focusable="true" android:minHeight="?attr/listPreferredItemHeight" android:orientation="horizontal" tools:ignore="MissingPrefix"> From ef50e3186aca20671d66b98eff663c826363155a Mon Sep 17 00:00:00 2001 From: Igor Date: Sun, 11 Oct 2020 22:35:37 +0300 Subject: [PATCH 58/72] Updated Need to be checked. --- app/src/main/res/values-ru-rRU/strings.xml | 488 ++++++++++++++++++--- 1 file changed, 417 insertions(+), 71 deletions(-) diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index ca112efec..ff961151f 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -1,85 +1,144 @@ - + Команда, ссылки на соц. сети + Основной цвет Основной цвет, по умолчанию фиолетовый + О программe + Добавить в избранное Добавить в очередь проигрывания Добавить в плейлист + Очистить очередь проигрывания Очистить плейлист + Режим повтора цикла + Удалить Удалить с устройства + Подробности + Перейти к альбому Перейти к исполнителю Перейти к жанру В начало + Разрешить + Размер сетки Размер сетки (по горизонтали) - Создать плейлист + + Новый плейлист + Далее + Играть Воспроизвести всё Играть далее Воспроизведение/Пауза + Предыдуший + Удалить из избранного Удалить из очереди воспроизведения Удалить из плейлиста + Переименовать + Сохранить очередь воспроизведения + Сканировать + Поиск + Запустить Задать в качества рингтона Установить как стартовый каталог + "Настройки" + Поделиться + Перемешать всё Перемешать плейлист + Таймер сна + Порядок сортировки + + Только альбомы исполнителей + Редактор тегов + Показать избранное Включить перемешивающий режим + Адаптированная + Добавить + Добавить тест песни + Добавить \nфото + "Добавить в плейлист" + Довавить текст песни + "В очередь добавлен 1 трек" + В очередь добавлено %1$d треков. + Альбом + Исполнитель альбома + Трек или альбом отсутствуют. + Альбомы + Всегда + Эй, попробуй этот крутой музыкальный плеер на: https://play.google.com/store/apps/details?id=%s + Перемешать - Часто прослушиваемые треки - Крупный - Карточка + Лучшие треки + + Полное изображение + Компактный Классический - Маленький - Текст + Маленькое изображение + Минималистичный + Исполнитель + Исполнители + Фокус на аудио отключен. - Отрегулировать настройки звука и эквалайзера + + Измените настройки звука и отрегулируйте настройки эквалайзера + Авто + Основная цветовая тема - Усиление баса + + Усиление басов + Биография + Биография + Чёрная + Черный список + Размытие + Карточка с размытием + Невозможно отправить отчет Недопустимый ключ доступа. Пожалуйста, свяжитесь с разработчиком приложения. Проблемы не активны в выбранном хранилище. Пожалуйста, свяжитесь с разработчиком приложения. @@ -91,91 +150,164 @@ Пожалуйста, введите ваш корректный пароль GitHub Пожалуйста, введите название отчета о проблеме Пожалуйста, введите корректно ваше имя пользователя GitHub - Произошла непредвиденная ошибка. Извините, если это продолжится -то «Очистите данные приложения» + Произошла непредвиденная ошибка. Извините, если это продолжится то \"Очистите данные приложения\" или отправьте сообщение на электронную почту Загрузка отчета на GitHub… Отправить с помощью учетной записи GitHub + Купить сейчас + Отменить + Карточка + Круговой + Цветная карточка + Карточка + + Квадратная Карточка + Карусель + Эффект карусели на экране воспроизведения + Каскадный - Передать на устройство - Список изменений + + Транслировать + + Список изменений + Список изменений находится на канале Telegram + Круг + Круговая - Классический + + Классическая + Очистить + Очистить данные приложения + Очистить черный список + Очистить очередь + Очистить плейлист - %1$s? \u042d\u0442\u043e \u043d\u0435\u043b\u044c\u0437\u044f \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c!]]> + %1$s? Это действие отменить невозможно!]]> + Закрыть + Цветная + Цвет + Цвета + Композитор + Скопировать информацию об устройстве в буфер обмена. - \u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442. - "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0443\u044e \u043e\u0431\u043b\u043e\u0436\u043a\u0430 \u0430\u043b\u044c\u0431\u043e\u043c\u0430." + + Не удалось создать плейлист + "Не удалось загрузить подходящую обложку для альбома." Не удалось восстановить покупку. - Не удалось отсканировать %d файлов. + Не удалось просканировать %d файлов. + Создать + Создан плейлист %1$s. + Участники и помощники + Сейчас играет %1$s от %2$s. + Тёмная - Нет текста + + Нет текста песни + Удалить плейлист %1$s?]]> + Удалить плейлисты - Удалить трек - %1$s?]]> - Удалить треки + + Удалить песню + %1$s?]]> + + Удалить песни + %1$d плейлистов?]]> - %1$d треков?]]> - Удалено %1$d треков. + %1$d песен?]]> + Удалено %1$d песен. + Удаление песен + Глубина + Описание + Информация об устройстве + Разрешить Retro Music изменять настройки звука + Выбрать рингтон - Хотите очистить черный список? - %1$s из черного списка?]]> + + Вы хотите очистить черный список? + %1$s из черного списка?]]> + Пожертвовать - Если вы считаете, что я заслуживаю награды за свою работу, можете отправить мне несколько рублей здесь + + Если вы считаете, что я заслуживаю награды за свой труд, можете отправить мне несколько рублей здесь + Купить мне: + Загрузить с Last.fm + Режим вождения + Редактировать + Изменить обложку + Пусто + Эквалайзер + Ошибка + ЧаВО + Избранное - Закончить последнюю песню + + Закончить вопсроизведение последней песни + По размеру + Плоская + Папки + Системная + Для вас - Бесплатная + + Доступно + Заполнение + Заполненная карточка - Изменить тему и цвета приложения + + Измените тему и цвет в приложении Внешний вид интерфейса + Жанр + Жанры + Развивайте проект на GitHub - Присоединяйся к сообществу Google Plus, где ты можешь попросить о помощи или следить за обновлениями Retro Music + + Присоединяйтесь к сообществу GooglePlus, где вы можете попросить о помощи или следить за обновлениями Retro Music + 1 2 3 @@ -184,102 +316,188 @@ 6 7 8 + Стиль сетки + Пластинки + История + Главная + Горизонтальный поворот + Изображение + Градиентное изображение + Изменение настроек загрузки изображения артиста + В плейлист %2$s внесено %1$d песен. + + Instagram Поделитесь своим обзором на приложение Retro Music в Instagram + Клавиатура + Битрейт + Формат Имя файла Расположение файла Размер + Больше от %s + Частота дискретизации + Длина + Показывать всегда + Последние добавленные - Предыдущая песня + + Последняя песня + Давайте послушаем немного музыки + Библиотека + Разделы библиотеки + Лицензии + Белая + Слушатели + Список файлов + Загрузка товаров... + Войти - Текст + + Текст песни + Сделано с ❤️ в Индии + Material + Ошибка + Ошибка разрешения + Моё имя + Любимые треки + Никогда - Новая фото баннера + + Новое фото баннера + Новый плейлист + Новое фото профиля + %s новая стартовая директория. - Следующая песня + + Следующая песня + Альбомы отсутствуют - Исполнители отсутствуют + + Исполнители отсутствую + "Сначала проиграйте песню, затем попробуйте заново." - Эквалайзер не найден. + + Эквалайзер не найден + Жанры отсутствуют + Текст отсутствует + Нет проигрываемых песен + Плейлисты отсутствуют + Покупки отсутствуют. + Нет результатов + Нет песен + Обычная + Обычный текст - Нормальный - %s не найден в media store.]]> - Нечего сканировать. - Нечего сканировать + + Обычный + + %s не найден в хранилище медиа.]]> + + Нет файлов для сканирования. + Нет файлов для сканирования + Уведомления + Настроить стиль уведомлений + Экран воспроизведения Очередь в \"Экране воспроизведения\" - Настроить экран текущего воспроизведения + Настроить экран воспроизведения 9+ тем экрана текущего воспроизведения + Только по Wi-Fi - Расширеные возможности + + Эксперементальные возможности + Другое + Пароль + Последние 3 месяца + Вставьте тест песни сюда + Панель снизу + Разрешение для доступа у внешнему хранилищу не получено. + Разрешения не получены. + Персонализация - Настройте управление экрана текущего воспроизведения и интерфейса - Взять из хранилища + + Настройте управление экрана воспроизведения и интерфейс управления музыкой + + Выбрать из хранилища + Выбрать изображение + Pinterest - Следуйте за страницей Retro Music в Pinterest + Подпишитесь на страницу Retro Music в Pinterest + Гладкая - Уведомление о песне предоставляет действия для воспроизведения / паузы и т.д. + + Уведомление о песне предоставляет действия для воспроизведения/паузы и т.д. Уведомления воспроизведения + Пустой плейлист + Плейлист пуст + Название плейлиста + Плейлисты + Стиль деталей альбома + Степень размытия в соответствующих темах; чем ниже, тем быстрее работает устройство Степень размытия + Регулировка углов нижней панели Диалоговое окно + Фильтровать песни по длине Фильтровать песни по длительности + Расширенные настройки Стиль альбома Звук @@ -290,14 +508,19 @@ Библиотека Экран блокировки Плейлисты - Автоматически ставит музыку на паузу при уменьшении громкости до нуля и играет после увеличения громкости. Предупреждение, когда вы увеличиваете громкость не в приложении, то Retro Music также начнёт воспроизведение + + Ставит музыку на паузу при уменьшении громкости до нуля и играет после увеличения громкости. Предупреждение, когда вы увеличиваете громкость не в приложении, то Retro Music также начнёт воспроизведение Пауза при нулевой громкости Имейте в виду, что включение этой функции может повлиять на заряд батареи Оставить экран включенным + Нажмите, чтобы открыть экран воспроизведения с прозрачной навигации или проведите чтобы открыть без прозрачной навигации Нажмите или Проведите + Эффект снегопада - Использовать обложку альбома текущей песни в качестве обоев на экране блокировки. + + Только альбомы исполнителей + Использовать обложку альбома текущей песни в качестве обоев на экране блокировки. Снизить громкость воспроизведения когда приходит звуковое уведомление Содержимое черного списка скрыто из вашей библиотеки. Начать воспроизведение сразу же после подключения Bluetooth-устройства @@ -307,7 +530,7 @@ Цвет кнопок фона и кнопок управления изменяется в соответствии с обложкой альбома с экрана воспроизведения Окрашивает ярлыки в основной цвет. Каждый раз, когда вы меняете цвет, вкл-выкл эту настройку, чтобы изменение вступило в силу Окрашивает панель навигации в главный цвет - "\u041e\u043a\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u0432 \u044f\u0440\u043a\u0438\u0439 \u0446\u0432\u0435\u0442 \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u0430\u043b\u044c\u0431\u043e\u043c\u0430" + "Окрашивает уведомление в доминирубщий цвет обложки альбома" Согласно Material Design в темном режиме цвета должны быть немного обесцвечены Наиболее доминирующий цвет будет выбран из обложки альбома или исполнителя Добавить дополнительные элементы управления для мини-плеера @@ -325,7 +548,9 @@ Начать воспроизведение музыки сразу после подключения наушников Режим перемешивания выключится при проигрывании нового списка песен Если доступно достаточно места, показывать регулировку громкости на экране воспроизведения - Показать обложку альбома + + Показывать обложку альбома в разделе исполнители + Показать обложку альбома Тема обложки альбома Стиль смены обложки альбома Сетка альбомов @@ -347,7 +572,8 @@ Тема приложения Показать вкладку жанра Сетка исполнителя на Главной странице - Кнопка Домой + Сетка альбома на Главной странице + Кнопка Домой Игнорировать обложки из хранилища Дата последнего добавления плейлиста Полноэкранное управление @@ -364,42 +590,74 @@ Режим перемешивания Регулировка громкости Информация о пользователе + Основной цвет - Основной цвет темы, по умолчанию - синий, теперь работает с темными цветами + Основной цвет темы, по умолчанию - серо-синий, теперь работает с темными цветами + Pro + Черная тема, Темы экрана воспроизведения, Эффект карусели и многое другое.. + Профиль + Купить - * Подумайте, прежде чем покупать, не просите возврата. + + *Подумайте, прежде чем покупать, не просите возврата. + Очередь + Оценить приложение + Понравилось это приложение? Напишите нам в Google Play Store о том, как мы можем сделать его еще лучше + Последние альбомы + Последние исполнители + Удалить + Удалить фотографию баннера + Удалить обложку + Удалить из черного списка + Удалить фотографию профиля + Удалить песню из плейлиста %1$s из плейлиста?]]> + Удалить песни из плейлиста + + Переименовать плейлист - Сообщить о проблеме + + Сообщить об ошибке + Сообщить об ошибке + Сбросить + Сбросить изображение исполнителя + Восстановить + Предыдущая покупка восстановлена. Перезагрузите приложение, чтобы использовать все функции. Восстановленные предыдущие покупки. + Восстановление покупки ... + Эквалайзер Retro Music + Retro Music Player Retro Music Pro + Ошибка при удалении файла: %s + Не удается получить SAF URI + Открыть навигационное меню Включите «Показать SD-карту» в всплывающем меню @@ -408,38 +666,68 @@ Выберите SD-карту в меню навигации Не открывайте никакие подпапки Нажмите кнопку «выбрать» в нижней части экрана + Ошибка при записи файла: %s + Сохранить + Сохранить как... + Сохранить как... + Сохраненный список воспроизведения в %s. + Сохранение изменений + Сканировать медиа-файлы + Просканировано %1$d из %2$d файлов. + Скробблинг + Найдите свою библиотеку ... + Выбрать все + Выбрать фото баннера + Выбранная кнопка + Отправить лог ошибки + Установить + Установить изображение исполнителя + Выбрать фото профиля + Поделиться приложением + Поделиться в Историях + Перемешать + Простая + Таймер отключения отменен. Таймер сна установлен на %d минут. + Провести + Маленький альбом - Общее + + Социальный + Поделиться историей + Песня + Длительность песни + Песни + Порядок сортировки По возрастанию Альбом @@ -449,103 +737,161 @@ Дата изменения Год По убыванию + Извините! Ваше устройство не поддерживает ввод с помощью речи + Поиск в вашей библиотеке + Стэк + Начать воспроизведение музыки. + Предложения + Просто покажите свое имя на главном экране + Поддержать разработку + Проведите, чтобы разблокировать + Синхронизируемый текст + Системный эквалайзер + Telegram Присоединяйтесь к группе Telegram, чтобы обсуждать ошибки, предлагать улучшения, хвастаться и т.д. + Спасибо ! + Аудиофайл + Этот месяц + Это неделя + Этот год + Маленькая + Название + Панель приборов + Добрый день Добрый день Добрый вечер Доброе утро Доброй ночи + Как тебя зовут + Сегодня + Топ альбомов + Топ исполнителей + "Трек (2 для трека 2 или 3004 для CD3 трека 4)" + Номер трека + Переведите + Помогите нам перевести приложение на ваш язык + Твиттер Поделитесь своим дизайном Retro Music + Не показывать - \u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u044d\u0442\u0443 \u043f\u0435\u0441\u043d\u044e. + + Невозможно проиграть эту песню + Следующие песни + Обновить изображение - Обновленяется ... + + Обновляется… + Имя пользователя + Версия + Вертикальный поворт + Виртуализация + Громкость + Поиск в интернете + Добро пожаловать, + Чем вы хотите поделиться? + Что нового : + Окно + Закругленные углы + Установите %1$s в качестве мелодии звонка. + Выбрано %1$d + Год + Выберите хотя бы одну категорию. + Вы будете перенаправлены на сайт системы отслеживания ошибок. + Данные вашей учетной записи используются только для аутентификации. Количество Примечание (необязательно) Начать оплату Показать экран воспроизведения Нажатие на уведомление будет показывать экран воспроизведения вместо домашнего экрана + Крошечная карточка О программе %s Выберите язык Переводчики Люди которые помогали переводить это приложение Попробуйте Retro Music Premium - + Поделитесь приложением со своими друзьями и родственниками + Нужна ещё помощь? + Градиент + Имя Пользователя + Сейчас не проигрывается + Последние 7 дней + + Песня - Песни - Песен Песни Альбом - Альбомов - Альбомов Альбомы %d Песня - %d Песен - %d Песен - %d песен + %d Песен %d Альбом - %d Альбомов - %d Альбомов - %d альбомов + %d Альбомов %d Исполнитель - %d Исполнителей - %d Исполнителей %d исполнителей + + Привет пустой фрагмент + Готов + Импорт плейлиста + Импортирует все плейлисты, перечисленные в Android Media Store с песнями, если плейлисты уже существуют, песни будут объединены. + Импорт + Колличество песен + По возрастанию + Количество композиций по убыванию From acd55f249a63c57bc57dc45634db7a1dacae0263 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 01:30:36 +0530 Subject: [PATCH 59/72] Code refactor --- .../activities/base/AbsSlidingMusicPanelActivity.kt | 4 ++-- .../code/name/monkey/retromusic/extensions/ViewExtensions.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 7f6e223f1..20023d102 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -346,10 +346,10 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewCompat.setElevation(bottomNavigationView, 10f) if (isVisible) { bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs) - bottomNavigationView.translateXAnimate(0f) + bottomNavigationView.translateYAnimate(0f) } else { + bottomNavigationView.translateYAnimate(150f) bottomSheetBehavior.peekHeightAnimate(heightOfBar) - bottomNavigationView.translateXAnimate(500f) } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt index 674610d6b..d36d10fe9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ViewExtensions.kt @@ -50,7 +50,7 @@ fun EditText.appHandleColor(): EditText { return this } -fun View.translateXAnimate(value: Float) { +fun View.translateYAnimate(value: Float) { ObjectAnimator.ofFloat(this, "translationY", value) .apply { duration = 300 From fa819284d0aceb463dd2b61803044d0e1bf65654 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 01:56:14 +0530 Subject: [PATCH 60/72] Added label for Settings --- .../retromusic/activities/SettingsActivity.kt | 19 ++++++++++++++++++- app/src/main/res/values-ru-rRU/strings.xml | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt index 5fdada745..4dc879e05 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/SettingsActivity.kt @@ -17,6 +17,7 @@ package code.name.monkey.retromusic.activities import android.os.Bundle import android.view.MenuItem import androidx.navigation.NavController +import androidx.navigation.NavDestination import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.VersionUtils import code.name.monkey.retromusic.R @@ -43,10 +44,26 @@ class SettingsActivity : AbsBaseActivity(), ColorChooserDialog.ColorCallback { applyToolbar(toolbar) val navController: NavController = findNavController(R.id.contentFrame) navController.addOnDestinationChangedListener { _, _, _ -> - toolbar.title = navController.currentDestination?.label + toolbar.title = navController.currentDestination?.let { getStringFromDestination(it) } } } + private fun getStringFromDestination(currentDestination: NavDestination): String { + val idRes = when (currentDestination.id) { + R.id.mainSettingsFragment -> R.string.action_settings + R.id.audioSettings -> R.string.pref_header_audio + R.id.imageSettingFragment -> R.string.pref_header_images + R.id.notificationSettingsFragment -> R.string.notification + R.id.nowPlayingSettingsFragment -> R.string.now_playing + R.id.otherSettingsFragment -> R.string.others + R.id.personalizeSettingsFragment -> R.string.personalize + R.id.themeSettingsFragment -> R.string.general_settings_title + R.id.aboutActivity -> R.string.action_about + else -> R.id.action_settings + } + return getString(idRes) + } + override fun onSupportNavigateUp(): Boolean { return findNavController(R.id.contentFrame).navigateUp() || super.onSupportNavigateUp() } diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index ca112efec..36a09efb4 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -37,7 +37,7 @@ Запустить Задать в качества рингтона Установить как стартовый каталог - "Настройки" + Настройки Поделиться Перемешать всё Перемешать плейлист From 1a4905cd76b15bda8d9d42fe0010869d122b4095 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 13:08:17 +0530 Subject: [PATCH 61/72] Added permission strings --- app/src/main/res/layout/activity_permission.xml | 8 ++++---- app/src/main/res/values/strings.xml | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/activity_permission.xml b/app/src/main/res/layout/activity_permission.xml index 784da5c4b..a03ffe0c4 100644 --- a/app/src/main/res/layout/activity_permission.xml +++ b/app/src/main/res/layout/activity_permission.xml @@ -37,9 +37,9 @@ app:layout_constraintTop_toBottomOf="@id/divider" app:permissionButtonTitle="Grant access" app:permissionIcon="@drawable/ic_sd_storage" - app:permissionTitle="Storage Access" + app:permissionTitle="@string/permission_title" app:permissionTitleNumber="1" - app:permissionTitleSubTitle="The app needs permission to access your device storage for music files playing music" /> + app:permissionTitleSubTitle="@string/permission_summary" /> %d Artist %d Artists - - Hello blank fragment Done Import playlist It imports all playlists listed in the Android Media Store with songs, if the playlists already exists, the songs will get merged. @@ -894,4 +892,8 @@ Song count Ascending Song count desc + The app needs permission to access your device storage for playing music + Storage Access + Ringtone + The app needs permission to access your device settings in order to set music as Ringtone From cef955abe66f4247ec158d6b59f0c67e8a51b33d Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 13:11:29 +0530 Subject: [PATCH 62/72] Bump up the version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3afd3f0f8..efd4cdc8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10448 - versionName '4.0.0' + "_" + getDate() + versionCode 10502 + versionName '4.0.010' + "_" + getDate() multiDexEnabled true From 59df82b8bd798e811086810393093844768983b3 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 12 Oct 2020 10:54:41 +0300 Subject: [PATCH 63/72] Added permission strings --- app/src/main/res/values-ru-rRU/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index b5797cdcf..2bfa82871 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -57,7 +57,9 @@ Запустить Задать в качества рингтона Установить как стартовый каталог - Настройки + + "Настройки" + Поделиться Перемешать всё @@ -517,6 +519,7 @@ Эффект снегопада + Только альбомы исполнителей Использовать обложку альбома текущей песни в качестве обоев на экране блокировки. Снизить громкость воспроизведения когда приходит звуковое уведомление Содержимое черного списка скрыто из вашей библиотеки. @@ -889,4 +892,8 @@ Колличество песен По возрастанию Количество композиций по убыванию + Приложению требуется разрешение на доступ к внутренней памяти вашего устройства для воспроизведения музыки. + Доступ к внутренней памяти + Рингтон + Приложению требуется разрешение на доступ к настройкам вашего устройства, чтобы установить музыку в качестве рингтона. From 3e258714bbbccfb29e1588835f2f592cab5c06a3 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 13:27:11 +0530 Subject: [PATCH 64/72] Fix color not showing in Gradient --- app/build.gradle | 2 +- .../activities/base/AbsSlidingMusicPanelActivity.kt | 4 ++-- .../fragments/player/gradient/GradientPlayerFragment.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index efd4cdc8b..d2d5b6fa5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10502 + versionCode 10503 versionName '4.0.010' + "_" + getDate() multiDexEnabled true diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt index 20023d102..2d28cd368 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt @@ -345,11 +345,11 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity() { ViewCompat.setElevation(slidingPanel, 10f) ViewCompat.setElevation(bottomNavigationView, 10f) if (isVisible) { - bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs) + bottomSheetBehavior.peekHeight = heightOfBarWithTabs bottomNavigationView.translateYAnimate(0f) } else { bottomNavigationView.translateYAnimate(150f) - bottomSheetBehavior.peekHeightAnimate(heightOfBar) + bottomSheetBehavior.peekHeight = heightOfBar } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt index 3b6e40e73..816832830 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt @@ -224,7 +224,7 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play songInfo.setTextColor(lastDisabledPlaybackControlsColor) volumeFragment?.setTintableColor(lastPlaybackControlsColor.ripAlpha()) - ViewUtil.setProgressDrawable(progressSlider, color.primaryTextColor.ripAlpha(), true) + ViewUtil.setProgressDrawable(progressSlider, lastPlaybackControlsColor.ripAlpha(), true) updateRepeatState() updateShuffleState() From 9dc2da66e1f3bbbd39a35780516024746adccf4c Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 20:25:31 +0530 Subject: [PATCH 65/72] Fix app shortcuts click crash --- .../retromusic/activities/MainActivity.kt | 10 ++++++---- .../AppShortcutLauncherActivity.kt | 20 ++++++------------- .../appshortcuts/DynamicShortcutManager.kt | 10 +++++++--- .../shortcuttype/BaseShortcutType.kt | 4 ++-- .../shortcuttype/ShuffleAllShortcutType.kt | 16 ++++++--------- .../player/gradient/GradientPlayerFragment.kt | 2 +- .../smartplaylist/PlaylistIdGenerator.kt | 2 +- .../retromusic/service/MusicService.java | 6 ++++-- .../res/layout/fragment_gradient_player.xml | 4 ++++ app/src/main/res/values-ru-rRU/strings.xml | 1 - 10 files changed, 37 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index 1b147ef59..e02d925bc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -77,7 +77,6 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis override fun onCreate(savedInstanceState: Bundle?) { setDrawUnderStatusBar() super.onCreate(savedInstanceState) - setStatusbarColorAuto() setNavigationbarColorAuto() setLightNavigationBar(true) @@ -91,6 +90,12 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis if (!hasPermissions()) { findNavController(R.id.fragment_container).navigate(R.id.permissionFragment) } + + showPromotionalDialog() + } + + private fun showPromotionalDialog() { + } private fun setupNavigationController() { @@ -104,9 +109,6 @@ class MainActivity : AbsSlidingMusicPanelActivity(), OnSharedPreferenceChangeLis } navController.graph = navGraph NavigationUI.setupWithNavController(getBottomNavigationView(), navController) - navController.addOnDestinationChangedListener { _, _, _ -> - // appBarLayout.setExpanded(true, true) - } } override fun onSupportNavigateUp(): Boolean = diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt index 68d04c479..6ecd760d5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt @@ -20,6 +20,7 @@ import android.os.Bundle import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType import code.name.monkey.retromusic.appshortcuts.shortcuttype.TopTracksShortcutType +import code.name.monkey.retromusic.extensions.extraNotNull import code.name.monkey.retromusic.model.Playlist import code.name.monkey.retromusic.model.smartplaylist.LastAddedPlaylist import code.name.monkey.retromusic.model.smartplaylist.ShuffleAllPlaylist @@ -31,16 +32,7 @@ class AppShortcutLauncherActivity : Activity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - var shortcutType = SHORTCUT_TYPE_NONE - - // Set shortcutType from the intent extras - val extras = intent.extras - if (extras != null) { - shortcutType = extras.getInt(KEY_SHORTCUT_TYPE, SHORTCUT_TYPE_NONE) - } - - when (shortcutType) { + when (extraNotNull(KEY_SHORTCUT_TYPE, SHORTCUT_TYPE_NONE).value) { SHORTCUT_TYPE_SHUFFLE_ALL -> { startServiceWithPlaylist( SHUFFLE_MODE_SHUFFLE, ShuffleAllPlaylist() @@ -78,9 +70,9 @@ class AppShortcutLauncherActivity : Activity() { companion object { const val KEY_SHORTCUT_TYPE = "code.name.monkey.retromusic.appshortcuts.ShortcutType" - const val SHORTCUT_TYPE_SHUFFLE_ALL = 0 - const val SHORTCUT_TYPE_TOP_TRACKS = 1 - const val SHORTCUT_TYPE_LAST_ADDED = 2 - const val SHORTCUT_TYPE_NONE = 4 + const val SHORTCUT_TYPE_SHUFFLE_ALL = 0L + const val SHORTCUT_TYPE_TOP_TRACKS = 1L + const val SHORTCUT_TYPE_LAST_ADDED = 2L + const val SHORTCUT_TYPE_NONE = 4L } } diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt index 7de8c8eff..2b74eab35 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt @@ -32,7 +32,7 @@ class DynamicShortcutManager(private val context: Context) { this.context.getSystemService(ShortcutManager::class.java) private val defaultShortcuts: List - get() = Arrays.asList( + get() = listOf( ShuffleAllShortcutType(context).shortcutInfo, TopTracksShortcutType(context).shortcutInfo, LastAddedShortcutType(context).shortcutInfo @@ -58,8 +58,12 @@ class DynamicShortcutManager(private val context: Context) { icon: Icon, intent: Intent ): ShortcutInfo { - return ShortcutInfo.Builder(context, id).setShortLabel(shortLabel) - .setLongLabel(longLabel).setIcon(icon).setIntent(intent).build() + return ShortcutInfo.Builder(context, id) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIcon(icon) + .setIntent(intent) + .build() } fun reportShortcutUsed(context: Context, shortcutId: String) { diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt index 1be5edcbf..91fc8f410 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt @@ -33,11 +33,11 @@ abstract class BaseShortcutType(internal var context: Context) { * @param shortcutType Describes the type of shortcut to create (ShuffleAll, TopTracks, custom playlist, etc.) * @return */ - internal fun getPlaySongsIntent(shortcutType: Int): Intent { + internal fun getPlaySongsIntent(shortcutType: Long): Intent { val intent = Intent(context, AppShortcutLauncherActivity::class.java) intent.action = Intent.ACTION_VIEW val b = Bundle() - b.putInt(AppShortcutLauncherActivity.KEY_SHORTCUT_TYPE, shortcutType) + b.putLong(AppShortcutLauncherActivity.KEY_SHORTCUT_TYPE, shortcutType) intent.putExtras(b) return intent } diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt index 4d13055ec..21855cacd 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt @@ -26,20 +26,16 @@ import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity class ShuffleAllShortcutType(context: Context) : BaseShortcutType(context) { override val shortcutInfo: ShortcutInfo - get() = ShortcutInfo.Builder( - context, id - ).setShortLabel(context.getString(R.string.app_shortcut_shuffle_all_short)).setLongLabel( - context.getString(R.string.app_shortcut_shuffle_all_long) - ).setIcon( - AppShortcutIconGenerator.generateThemedIcon( - context, R.drawable.ic_app_shortcut_shuffle_all - ) - ).setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_SHUFFLE_ALL)) + get() = ShortcutInfo.Builder(context, id) + .setShortLabel(context.getString(R.string.app_shortcut_shuffle_all_short)) + .setLongLabel(context.getString(R.string.app_shortcut_shuffle_all_long)) + .setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_shuffle_all)) + .setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_SHUFFLE_ALL)) .build() companion object { val id: String - get() = BaseShortcutType.ID_PREFIX + "shuffle_all" + get() = ID_PREFIX + "shuffle_all" } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt index 816832830..cf11bd78d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt @@ -168,8 +168,8 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play } override fun onPause() { - recyclerViewDragDropManager?.cancelDrag() super.onPause() + recyclerViewDragDropManager?.cancelDrag() progressViewUpdateHelper.stop() } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/PlaylistIdGenerator.kt b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/PlaylistIdGenerator.kt index f81ceb6c7..baefff7b9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/PlaylistIdGenerator.kt +++ b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/PlaylistIdGenerator.kt @@ -6,7 +6,7 @@ import kotlin.math.abs object PlaylistIdGenerator { operator fun invoke(name: String, @DrawableRes iconRes: Int): Long { - return -abs(31L * name.hashCode() + iconRes * name.hashCode() * 31L * 31L) + return abs(31L * name.hashCode() + iconRes * name.hashCode() * 31L * 31L) } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index b7d399720..f0b8b110a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -72,8 +72,10 @@ import code.name.monkey.retromusic.appwidgets.AppWidgetText; import code.name.monkey.retromusic.glide.BlurTransformation; import code.name.monkey.retromusic.glide.SongGlideRequest; import code.name.monkey.retromusic.helper.ShuffleHelper; +import code.name.monkey.retromusic.model.AbsCustomPlaylist; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist; import code.name.monkey.retromusic.providers.HistoryStore; import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore; import code.name.monkey.retromusic.providers.SongPlayCountStore; @@ -1271,10 +1273,10 @@ public class MusicService extends Service } private void playFromPlaylist(Intent intent) { - Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); + AbsSmartPlaylist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); if (playlist != null) { - List playlistSongs = playlist.getSongs(); + List playlistSongs = playlist.songs(); if (!playlistSongs.isEmpty()) { if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { int startPosition = new Random().nextInt(playlistSongs.size()); diff --git a/app/src/main/res/layout/fragment_gradient_player.xml b/app/src/main/res/layout/fragment_gradient_player.xml index 6c6a3bfc9..f83beb934 100644 --- a/app/src/main/res/layout/fragment_gradient_player.xml +++ b/app/src/main/res/layout/fragment_gradient_player.xml @@ -108,6 +108,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="?attr/roundSelector" + android:clickable="true" + android:focusable="true" android:padding="14dp" android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@+id/previousButton" @@ -124,6 +126,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="?attr/roundSelector" + android:clickable="true" + android:focusable="true" android:padding="14dp" android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@+id/nextButton" diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 2bfa82871..8f08c5905 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -519,7 +519,6 @@ Эффект снегопада - Только альбомы исполнителей Использовать обложку альбома текущей песни в качестве обоев на экране блокировки. Снизить громкость воспроизведения когда приходит звуковое уведомление Содержимое черного списка скрыто из вашей библиотеки. From 52dc1d9f22ccd9465f8adbee79619d777d27d010 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 22:52:53 +0530 Subject: [PATCH 66/72] Fix scaning and updateing error --- app/build.gradle | 2 +- .../tageditor/AbsTagEditorActivity.kt | 70 ++--- .../tageditor/AlbumTagEditorActivity.kt | 8 +- .../tageditor/SongTagEditorActivity.kt | 8 +- .../tageditor/WriteTagsAsyncTask.java | 273 ++++++++---------- .../monkey/retromusic/extensions/ColorExt.kt | 4 + .../fragments/folder/FoldersFragment.java | 3 +- .../player/full/FullPlayerFragment.kt | 12 +- ...teToastMediaScannerCompletionListener.java | 81 +++--- .../monkey/retromusic/model/ArtworkInfo.kt | 5 + .../monkey/retromusic/model/LoadingInfo.kt | 9 + 11 files changed, 215 insertions(+), 260 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/model/ArtworkInfo.kt create mode 100644 app/src/main/java/code/name/monkey/retromusic/model/LoadingInfo.kt diff --git a/app/build.gradle b/app/build.gradle index d2d5b6fa5..ba496ad4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { } signingConfigs { release { - Properties properties = getProperties('/Users/apple/Documents/Github/retro.properties ') + Properties properties = getProperties('E:/Documents/GitHub/retro.properties') storeFile file(getProperty(properties, 'storeFile')) keyAlias getProperty(properties, 'keyAlias') storePassword getProperty(properties, 'storePassword') diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt index 9f40db14d..0202a4b4d 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt @@ -17,11 +17,9 @@ package code.name.monkey.retromusic.activities.tageditor import android.app.Activity import android.app.SearchManager import android.content.Intent -import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.os.Build import android.os.Bundle import android.util.Log import android.view.MenuItem @@ -30,25 +28,26 @@ import android.view.animation.OvershootInterpolator import androidx.appcompat.app.AlertDialog import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.ATHUtil -import code.name.monkey.appthemehelper.util.ColorUtil -import code.name.monkey.appthemehelper.util.MaterialValueHelper import code.name.monkey.appthemehelper.util.TintHelper import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R.drawable import code.name.monkey.retromusic.activities.base.AbsBaseActivity import code.name.monkey.retromusic.activities.saf.SAFGuideActivity +import code.name.monkey.retromusic.extensions.accentColor +import code.name.monkey.retromusic.model.ArtworkInfo +import code.name.monkey.retromusic.model.LoadingInfo import code.name.monkey.retromusic.repository.Repository import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.SAFUtil import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.File -import java.util.* import kotlinx.android.synthetic.main.activity_album_tag_editor.* import org.jaudiotagger.audio.AudioFile import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.tag.FieldKey import org.koin.android.ext.android.inject +import java.io.File +import java.util.* abstract class AbsTagEditorActivity : AbsBaseActivity() { val repository by inject() @@ -63,6 +62,8 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { private val currentSongPath: String? = null private var savedTags: Map? = null private var savedArtworkInfo: ArtworkInfo? = null + protected abstract val contentViewLayout: Int + protected abstract fun loadImageFromFile(selectedFile: Uri?) protected val show: AlertDialog get() = @@ -76,7 +77,6 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { } } .show() - protected abstract val contentViewLayout: Int internal val albumArtist: String? get() { @@ -196,6 +196,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { getIntentExtras() songPaths = getSongPaths() + println(songPaths?.size) if (songPaths!!.isEmpty()) { finish() } @@ -212,9 +213,9 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { private fun setUpImageView() { loadCurrentImage() items = listOf( - getString(code.name.monkey.retromusic.R.string.pick_from_local_storage), - getString(code.name.monkey.retromusic.R.string.web_search), - getString(code.name.monkey.retromusic.R.string.remove_cover) + getString(R.string.pick_from_local_storage), + getString(R.string.web_search), + getString(R.string.remove_cover) ) editorImage?.setOnClickListener { show } } @@ -225,7 +226,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { startActivityForResult( Intent.createChooser( intent, - getString(code.name.monkey.retromusic.R.string.pick_from_local_storage) + getString(R.string.pick_from_local_storage) ), REQUEST_CODE_SELECT_IMAGE ) } @@ -237,20 +238,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { protected abstract fun deleteImage() private fun setUpFab() { - saveFab.backgroundTintList = ColorStateList.valueOf(ThemeStore.accentColor(this)) - ColorStateList.valueOf( - MaterialValueHelper.getPrimaryTextColor( - this, - ColorUtil.isColorLight( - ThemeStore.accentColor( - this - ) - ) - ) - ).apply { - saveFab.setTextColor(this) - saveFab.iconTint = this - } + saveFab.accentColor() saveFab.apply { scaleX = 0f scaleY = 0f @@ -344,30 +332,19 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { RetroUtil.hideSoftKeyboard(this) hideFab() - - savedSongPaths = songPaths - savedTags = fieldKeyValueMap - savedArtworkInfo = artworkInfo - - if (!SAFUtil.isSAFRequired(savedSongPaths)) { - writeTags(savedSongPaths) - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (SAFUtil.isSDCardAccessGranted(this)) { - writeTags(savedSongPaths) - } else { - startActivityForResult( - Intent(this, SAFGuideActivity::class.java), - SAFGuideActivity.REQUEST_CODE_SAF_GUIDE - ) - } - } - } + println(fieldKeyValueMap) + WriteTagsAsyncTask(this).execute( + LoadingInfo( + songPaths, + fieldKeyValueMap, + artworkInfo + ) + ) } private fun writeTags(paths: List?) { WriteTagsAsyncTask(this).execute( - WriteTagsAsyncTask.LoadingInfo( + LoadingInfo( paths, savedTags, savedArtworkInfo @@ -375,6 +352,7 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { ) } + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) when (requestCode) { @@ -400,7 +378,6 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { } } - protected abstract fun loadImageFromFile(selectedFile: Uri?) private fun getAudioFile(path: String): AudioFile { return try { @@ -411,7 +388,6 @@ abstract class AbsTagEditorActivity : AbsBaseActivity() { } } - class ArtworkInfo constructor(val albumId: Long, val artwork: Bitmap?) companion object { diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt index 957413ffb..b48ab2961 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt @@ -32,6 +32,7 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.extensions.appHandleColor import code.name.monkey.retromusic.glide.palette.BitmapPaletteTranscoder import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper +import code.name.monkey.retromusic.model.ArtworkInfo import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.ImageUtil import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette @@ -177,8 +178,11 @@ class AlbumTagEditorActivity : AbsTagEditorActivity(), TextWatcher { writeValuesToFiles( fieldKeyValueMap, - if (deleteAlbumArt) ArtworkInfo(id, null) - else if (albumArtBitmap == null) null else ArtworkInfo(id, albumArtBitmap!!) + when { + deleteAlbumArt -> ArtworkInfo(id, null) + albumArtBitmap == null -> null + else -> ArtworkInfo(id, albumArtBitmap!!) + } ) } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt index 4446bcfee..15a370025 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt @@ -23,10 +23,10 @@ import code.name.monkey.appthemehelper.util.MaterialUtil import code.name.monkey.retromusic.R import code.name.monkey.retromusic.extensions.appHandleColor import code.name.monkey.retromusic.repository.SongRepository -import java.util.* import kotlinx.android.synthetic.main.activity_song_tag_editor.* import org.jaudiotagger.tag.FieldKey import org.koin.android.ext.android.inject +import java.util.* class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { @@ -102,11 +102,7 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { writeValuesToFiles(fieldKeyValueMap, null) } - override fun getSongPaths(): List { - val paths = ArrayList(1) - paths.add(songRepository.song(id).data) - return paths - } + override fun getSongPaths(): List = listOf(songRepository.song(id).data) override fun loadImageFromFile(selectedFile: Uri?) { } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java index 405819e60..45cb21a69 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/WriteTagsAsyncTask.java @@ -5,188 +5,147 @@ import android.app.Dialog; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.Build; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import code.name.monkey.retromusic.R; -import code.name.monkey.retromusic.misc.DialogAsyncTask; -import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; -import code.name.monkey.retromusic.util.MusicUtil; -import code.name.monkey.retromusic.util.SAFUtil; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; + +import com.afollestad.materialdialogs.MaterialDialog; + import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.audio.exceptions.CannotReadException; +import org.jaudiotagger.audio.exceptions.CannotWriteException; +import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; +import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.Tag; +import org.jaudiotagger.tag.TagException; import org.jaudiotagger.tag.images.Artwork; import org.jaudiotagger.tag.images.ArtworkFactory; -public class WriteTagsAsyncTask - extends DialogAsyncTask { +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; - private WeakReference activity; +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.misc.DialogAsyncTask; +import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener; +import code.name.monkey.retromusic.model.LoadingInfo; +import code.name.monkey.retromusic.util.MusicUtil; - public WriteTagsAsyncTask(@NonNull Activity activity) { - super(activity); - this.activity = new WeakReference<>(activity); - } +public class WriteTagsAsyncTask extends DialogAsyncTask> { - @NonNull - @Override - protected Dialog createDialog(@NonNull Context context) { + public WriteTagsAsyncTask(Context context) { + super(context); + } - return new MaterialAlertDialogBuilder(context) - .setTitle(R.string.saving_changes) - .setCancelable(false) - .setView(R.layout.loading) - .create(); - } - - @Override - protected String[] doInBackground(LoadingInfo... params) { - try { - LoadingInfo info = params[0]; - - Artwork artwork = null; - File albumArtFile = null; - if (info.artworkInfo != null && info.artworkInfo.getArtwork() != null) { + @Override + protected List doInBackground(LoadingInfo... params) { try { - albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile(); - info.artworkInfo - .getArtwork() - .compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile)); - artwork = ArtworkFactory.createArtworkFromFile(albumArtFile); - } catch (IOException e) { - e.printStackTrace(); - } - } + LoadingInfo info = params[0]; - int counter = 0; - boolean wroteArtwork = false; - boolean deletedArtwork = false; - for (String filePath : info.filePaths) { - publishProgress(++counter, info.filePaths.size()); - try { - Uri safUri = null; - if (filePath.contains(SAFUtil.SEPARATOR)) { - String[] fragments = filePath.split(SAFUtil.SEPARATOR); - filePath = fragments[0]; - safUri = Uri.parse(fragments[1]); - } - - AudioFile audioFile = AudioFileIO.read(new File(filePath)); - Tag tag = audioFile.getTagOrCreateAndSetDefault(); - - if (info.fieldKeyValueMap != null) { - for (Map.Entry entry : info.fieldKeyValueMap.entrySet()) { - try { - tag.setField(entry.getKey(), entry.getValue()); - } catch (Exception e) { - e.printStackTrace(); - } + Artwork artwork = null; + File albumArtFile = null; + if (info.getArtworkInfo() != null && info.getArtworkInfo().getArtwork() != null) { + try { + albumArtFile = MusicUtil.INSTANCE.createAlbumArtFile().getCanonicalFile(); + info.getArtworkInfo().getArtwork().compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile)); + artwork = ArtworkFactory.createArtworkFromFile(albumArtFile); + } catch (IOException e) { + e.printStackTrace(); + } } - } - if (info.artworkInfo != null) { - if (info.artworkInfo.getArtwork() == null) { - tag.deleteArtworkField(); - deletedArtwork = true; - } else if (artwork != null) { - tag.deleteArtworkField(); - tag.setField(artwork); - wroteArtwork = true; + int counter = 0; + boolean wroteArtwork = false; + boolean deletedArtwork = false; + for (String filePath : info.getFilePaths()) { + publishProgress(++counter, info.getFilePaths().size()); + try { + AudioFile audioFile = AudioFileIO.read(new File(filePath)); + Tag tag = audioFile.getTagOrCreateAndSetDefault(); + + if (info.getFieldKeyValueMap() != null) { + for (Map.Entry entry : info.getFieldKeyValueMap().entrySet()) { + try { + tag.setField(entry.getKey(), entry.getValue()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + if (info.getArtworkInfo() != null) { + if (info.getArtworkInfo().getArtwork() == null) { + tag.deleteArtworkField(); + deletedArtwork = true; + } else if (artwork != null) { + tag.deleteArtworkField(); + tag.setField(artwork); + wroteArtwork = true; + } + } + + audioFile.commit(); + } catch (@NonNull CannotReadException | IOException | CannotWriteException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { + e.printStackTrace(); + } } - } - Activity activity = this.activity.get(); - SAFUtil.write(activity, audioFile, safUri); + Context context = getContext(); + if (context != null) { + if (wroteArtwork) { + MusicUtil.INSTANCE.insertAlbumArt(context, info.getArtworkInfo().getAlbumId(), albumArtFile.getPath()); + } else if (deletedArtwork) { + MusicUtil.INSTANCE.deleteAlbumArt(context, info.getArtworkInfo().getAlbumId()); + } + } - } catch (@NonNull Exception e) { - e.printStackTrace(); + return info.getFilePaths(); + } catch (Exception e) { + e.printStackTrace(); + return null; } - } + } - Context context = getContext(); - if (context != null) { - if (wroteArtwork) { - MusicUtil.INSTANCE.insertAlbumArt( - context, info.artworkInfo.getAlbumId(), albumArtFile.getPath()); - } else if (deletedArtwork) { - MusicUtil.INSTANCE.deleteAlbumArt(context, info.artworkInfo.getAlbumId()); + @Override + protected void onPostExecute(List toBeScanned) { + super.onPostExecute(toBeScanned); + scan(toBeScanned); + } + + @Override + protected void onCancelled(List toBeScanned) { + super.onCancelled(toBeScanned); + scan(toBeScanned); + } + + private void scan(List toBeScanned) { + Context context = getContext(); + if (toBeScanned == null || toBeScanned.isEmpty()) { + Log.i("scan", "scan: Empty"); + Toast.makeText(context, "Scan file from folder", Toast.LENGTH_SHORT).show(); + return; } - } - - Collection paths = info.filePaths; - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { - paths = new ArrayList<>(info.filePaths.size()); - for (String path : info.filePaths) { - if (path.contains(SAFUtil.SEPARATOR)) { - path = path.split(SAFUtil.SEPARATOR)[0]; - } - paths.add(path); - } - } - - return paths.toArray(new String[paths.size()]); - } catch (Exception e) { - e.printStackTrace(); - return null; + MediaScannerConnection.scanFile(context, toBeScanned.toArray(new String[0]), null, context instanceof Activity ? new UpdateToastMediaScannerCompletionListener((Activity) context, toBeScanned) : null); } - } - @Override - protected void onCancelled(String[] toBeScanned) { - super.onCancelled(toBeScanned); - scan(toBeScanned); - } - - @Override - protected void onPostExecute(String[] toBeScanned) { - super.onPostExecute(toBeScanned); - scan(toBeScanned); - } - - @Override - protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) { - super.onProgressUpdate(dialog, values); - // ((MaterialDialog) dialog).setMaxProgress(values[1]); - // ((MaterialDialog) dialog).setProgress(values[0]); - } - - private void scan(String[] toBeScanned) { - Activity activity = this.activity.get(); - if (activity != null) { - MediaScannerConnection.scanFile( - activity, - toBeScanned, - null, - new UpdateToastMediaScannerCompletionListener(activity, toBeScanned)); + @Override + protected Dialog createDialog(@NonNull Context context) { + return new MaterialDialog.Builder(context) + .title(R.string.saving_changes) + .cancelable(false) + .progress(false, 0) + .build(); } - } - public static class LoadingInfo { - - @Nullable final Map fieldKeyValueMap; - - final Collection filePaths; - - @Nullable private AbsTagEditorActivity.ArtworkInfo artworkInfo; - - public LoadingInfo( - Collection filePaths, - @Nullable Map fieldKeyValueMap, - @Nullable AbsTagEditorActivity.ArtworkInfo artworkInfo) { - this.filePaths = filePaths; - this.fieldKeyValueMap = fieldKeyValueMap; - this.artworkInfo = artworkInfo; + @Override + protected void onProgressUpdate(@NonNull Dialog dialog, Integer... values) { + super.onProgressUpdate(dialog, values); + ((MaterialDialog) dialog).setMaxProgress(values[1]); + ((MaterialDialog) dialog).setProgress(values[0]); } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt b/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt index ef6085c44..d3bfb6dfa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt +++ b/app/src/main/java/code/name/monkey/retromusic/extensions/ColorExt.kt @@ -154,6 +154,10 @@ fun MaterialButton.applyColor(color: Int) { iconTint = textColorColorStateList } +fun MaterialButton.accentColor(){ + this.applyColor(ThemeStore.accentColor(context)) +} + fun MaterialButton.applyOutlineColor(color: Int) { val textColorColorStateList = ColorStateList.valueOf(color) setTextColor(textColorColorStateList) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java index 9a839da69..1c6dbf95c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/folder/FoldersFragment.java @@ -54,6 +54,7 @@ import java.io.FileFilter; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -520,7 +521,7 @@ public class FoldersFragment extends AbsMainActivityFragment getActivity().getApplicationContext(), toBeScanned, null, - new UpdateToastMediaScannerCompletionListener(getActivity(), toBeScanned)); + new UpdateToastMediaScannerCompletionListener(getActivity(), Arrays.asList(toBeScanned))); } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt index fc7770dd3..925144726 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt @@ -22,7 +22,6 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.core.os.bundleOf -import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.retromusic.EXTRA_ARTIST_ID @@ -163,13 +162,10 @@ class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full), private fun setupArtist() { artistImage.setOnClickListener { mainActivity.collapsePanel() - findNavController() - .navigate( - R.id.artistDetailsFragment, - bundleOf(EXTRA_ARTIST_ID to MusicPlayerRemote.currentSong.artistId), - null, - FragmentNavigatorExtras(it to "artist") - ) + findNavController().navigate( + R.id.artistDetailsFragment, + bundleOf(EXTRA_ARTIST_ID to MusicPlayerRemote.currentSong.artistId), + ) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java b/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java index 03a45cac2..1b6dd0233 100644 --- a/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java +++ b/app/src/main/java/code/name/monkey/retromusic/misc/UpdateToastMediaScannerCompletionListener.java @@ -19,49 +19,54 @@ import android.app.Activity; import android.media.MediaScannerConnection; import android.net.Uri; import android.widget.Toast; -import code.name.monkey.retromusic.R; + import java.lang.ref.WeakReference; +import java.util.List; -/** @author Karim Abou Zeid (kabouzeid) */ +import code.name.monkey.retromusic.R; + +/** + * @author Karim Abou Zeid (kabouzeid) + */ public class UpdateToastMediaScannerCompletionListener - implements MediaScannerConnection.OnScanCompletedListener { + implements MediaScannerConnection.OnScanCompletedListener { - private final WeakReference activityWeakReference; + private final WeakReference activityWeakReference; - private final String couldNotScanFiles; - private final String scannedFiles; - private final String[] toBeScanned; - private int failed = 0; - private int scanned = 0; - private Toast toast; + private final String couldNotScanFiles; + private final String scannedFiles; + private final List toBeScanned; + private int failed = 0; + private int scanned = 0; + private Toast toast; - @SuppressLint("ShowToast") - public UpdateToastMediaScannerCompletionListener(Activity activity, String[] toBeScanned) { - this.toBeScanned = toBeScanned; - scannedFiles = activity.getString(R.string.scanned_files); - couldNotScanFiles = activity.getString(R.string.could_not_scan_files); - toast = Toast.makeText(activity.getApplicationContext(), "", Toast.LENGTH_SHORT); - activityWeakReference = new WeakReference<>(activity); - } - - @Override - public void onScanCompleted(final String path, final Uri uri) { - Activity activity = activityWeakReference.get(); - if (activity != null) { - activity.runOnUiThread( - () -> { - if (uri == null) { - failed++; - } else { - scanned++; - } - String text = - " " - + String.format(scannedFiles, scanned, toBeScanned.length) - + (failed > 0 ? " " + String.format(couldNotScanFiles, failed) : ""); - toast.setText(text); - toast.show(); - }); + @SuppressLint("ShowToast") + public UpdateToastMediaScannerCompletionListener(Activity activity, List toBeScanned) { + this.toBeScanned = toBeScanned; + scannedFiles = activity.getString(R.string.scanned_files); + couldNotScanFiles = activity.getString(R.string.could_not_scan_files); + toast = Toast.makeText(activity.getApplicationContext(), "", Toast.LENGTH_SHORT); + activityWeakReference = new WeakReference<>(activity); + } + + @Override + public void onScanCompleted(final String path, final Uri uri) { + Activity activity = activityWeakReference.get(); + if (activity != null) { + activity.runOnUiThread( + () -> { + if (uri == null) { + failed++; + } else { + scanned++; + } + String text = + " " + + String.format(scannedFiles, scanned, toBeScanned.size()) + + (failed > 0 ? " " + String.format(couldNotScanFiles, failed) : ""); + toast.setText(text); + toast.show(); + }); + } } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/ArtworkInfo.kt b/app/src/main/java/code/name/monkey/retromusic/model/ArtworkInfo.kt new file mode 100644 index 000000000..a0bfead2b --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/model/ArtworkInfo.kt @@ -0,0 +1,5 @@ +package code.name.monkey.retromusic.model + +import android.graphics.Bitmap + +class ArtworkInfo constructor(val albumId: Long, val artwork: Bitmap?) diff --git a/app/src/main/java/code/name/monkey/retromusic/model/LoadingInfo.kt b/app/src/main/java/code/name/monkey/retromusic/model/LoadingInfo.kt new file mode 100644 index 000000000..bfb9751f3 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/model/LoadingInfo.kt @@ -0,0 +1,9 @@ +package code.name.monkey.retromusic.model + +import org.jaudiotagger.tag.FieldKey + +class LoadingInfo( + val filePaths: List?, + val fieldKeyValueMap: Map?, + val artworkInfo: ArtworkInfo? +) \ No newline at end of file From 4fdb33644455af2889ea3a9ab1921babacf248e8 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Mon, 12 Oct 2020 22:58:39 +0530 Subject: [PATCH 67/72] Artist album changes to card --- .../retromusic/adapter/album/HorizontalAlbumAdapter.kt | 2 +- .../monkey/retromusic/helper/HorizontalAdapterHelper.kt | 4 ++-- app/src/main/res/layout/item_image.xml | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt index 26a1958c7..8a94b7a5f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt @@ -65,7 +65,7 @@ class HorizontalAlbumAdapter( } override fun getItemViewType(position: Int): Int { - return HorizontalAdapterHelper.getItemViewtype(position, itemCount) + return HorizontalAdapterHelper.getItemViewType(position, itemCount) } override fun getItemCount(): Int { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt index b155cbd99..f98508bf8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/HorizontalAdapterHelper.kt @@ -20,7 +20,7 @@ import code.name.monkey.retromusic.R object HorizontalAdapterHelper { - const val LAYOUT_RES = R.layout.item_album_card + const val LAYOUT_RES = R.layout.item_image private const val TYPE_FIRST = 1 private const val TYPE_MIDDLE = 2 @@ -40,7 +40,7 @@ object HorizontalAdapterHelper { } } - fun getItemViewtype(position: Int, itemCount: Int): Int { + fun getItemViewType(position: Int, itemCount: Int): Int { return when (position) { 0 -> TYPE_FIRST itemCount - 1 -> TYPE_LAST diff --git a/app/src/main/res/layout/item_image.xml b/app/src/main/res/layout/item_image.xml index 1232577de..2a3f434a0 100644 --- a/app/src/main/res/layout/item_image.xml +++ b/app/src/main/res/layout/item_image.xml @@ -7,14 +7,15 @@ android:background="?rectSelector" android:orientation="vertical"> - @@ -26,7 +27,7 @@ android:scaleType="centerCrop" tools:srcCompat="@tools:sample/backgrounds/scenic[16]" /> - + Date: Mon, 12 Oct 2020 19:51:36 +0200 Subject: [PATCH 68/72] Update Polish translation v1 --- app/src/main/res/values-pl-rPL/strings.xml | 1037 +++++++++++------ .../res/values-pl-rPL/strings_placeholder.xml | 550 +++++++++ 2 files changed, 1243 insertions(+), 344 deletions(-) create mode 100644 app/src/main/res/values-pl-rPL/strings_placeholder.xml diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 46f655c70..b8efb1210 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -1,180 +1,313 @@ - + Zespół, media społecznościowe + Kolor akcentu - Kolor akcentu motywu, domyślnie morski + Kolor akcentu motywu, domyślnie fioletowy + O aplikacji + Dodaj do ulubionych Dodaj do kolejki Dodaj do playlisty + Wyczyść kolejkę Wyczyść playlistę + Przełącz tryb powtarzania + Usuń Usuń z urządzenia + Szczegóły - Przejdź do albumu - Przejdź do artysty + + Idź do albumu + Idź do artysty Idź do gatunku - Przejdź do katalogu startowego + Idź do katalogu startowego + Przyznaj + Rozmiar siatki Rozmiar siatki (poziomo) + Nowa playlista + Następny + Odtwarzaj Odtwórz wszystko Odtwarzaj następne Odtwarzanie/Pauza + Poprzedni + Usuń z ulubionych Usuń z kolejki odtwarzania Usuń z playlisty + Zmień nazwę + Zapisz kolejkę + Skanuj + Szukaj + Rozpocznij Ustaw jako dzwonek Ustaw jako katalog startowy + "Ustawienia" + Udostępnij - Losowo wszystko + + Wszystko losowo Playlista losowo + Wyłącznik czasowy - Sortowanie + + Sortuj według + + Tylko artyści albumów + Edytor tagów - Przełącz na ulubione + + Przełącz ulubione Przełącz tryb losowy + Adaptacyjny + Dodaj + Dodaj tekst utworu - Dodaj\nzdjęcie + + Dodaj \nzdjęcie + "Dodaj do playlisty" + Dodaj znaczniki czasowe do tekstu - "Dodano 1 tytuł do kolejki odtwarzania" - Dodano %1$d tytułów do kolejki + + "Dodano 1 tytuł do kolejki odtwarzania." + + Dodano %1$d tytułów do kolejki. + Album + Artysta albumu - Pole tytuł lub artysta jest puste. + + Tytuł lub artysta jest pusty. + Albumy + Zawsze + Hej sprawdź ten fajny odtwarzacz muzyki na: https://play.google.com/store/apps/details?id=%s + Losowo Najczęściej odtwarzane utwory - Duży + + Pełne zdjęcie Karta Klasyczny - mały + Mały Tekst + Artysta + Artyści + Odrzucono fokus dźwiękowy. + Dostosuj ustawienia dźwięku i equalizera + Automatyczny + Bazowy kolor motywu + Wzmocnienie Bassu - Biografia + + Bio + Biografia + Po prostu czarny + Czarna lista + Rozmycie + Rozmyta Karta + Nie udało się wysłać raportu Niepoprawny token dostępu. Proszę się skontaktować z twórcą aplikacji. Problemy nie zostały włączone dla wybranego repozytorium. Proszę się skontaktować z twórcą aplikacji. Wystąpił nieoczekiwany błąd. Proszę skontaktować się z twórcą aplikacji. Zła nazwa użytkownika lub hasło - Problem + Błąd Wyślij ręcznie - Proszę opisać problemu + Proszę opisać problem Proszę wpisać poprawne hasło GitHub Proszę wpisać tytuł problemu Proszę wpisać poprawną nazwę użytkownika GitHub - Wystąpił nieoczekiwany błąd. Wyczyść dane podręczne lub - jeśli błąd pojawi się ponownie - wyślij nam maila. - Wrzucanie raportu na GitHub... + Wystąpił nieoczekiwany błąd. Wyczyść dane podręczne lub jeśli błąd pojawi się ponownie - wyślij nam maila + Wrzucanie raportu na GitHub… Wyślij przez konto GitHub + Kup teraz + Anuluj + Karta + Okrągły - Kolorowa Karta + + Kolorowa karta + Karta - Karuzela - Efekt karuzeli na ekranie Teraz odtwarzane + + Kwadratowa karta + + Karozela + + Efekt karuzeli na ekranie \"Teraz odtwarzane\" + Kaskadowy + Strumieniowanie + Lista zmian + Lista zmian zarządzana z aplikacji Telegram + Okręg + Okrągły + Klasyczny + Wyczyść + Wyczyść dane aplikacji + Wyczyść czarną listę + Wyczyść kolejkę + Wyczyść playlistę %1$s? To nie może być cofnięte!]]> + Zamknij + Kolor + Kolor + Kolory + Kompozytor - Skopiowano informacje o urządzeniu do schowka - Nie mo\u017cna utworzy\u0107 playlisty. - "Nie mo\u017cna pobra\u0107 dopasowanej ok\u0142adki albumu" - Nie można przywrócić zakupów. + + Skopiowano informacje o urządzeniu do schowka. + + Nie można utworzyć playlisty. + "Nie można pobrać pasującej okładni albumu." + Nie można przywrócić zakupu. Nie można przeskanować %d plików. + Utwórz - Stworzono playlistę %1$s. - Członkowie i współpracownicy + + Utworzono playlistę %1$s. + + Członkowie i współpracownicy + Aktualnie odtwarzane %1$s wykonawcy %2$s. + Dość ciemny - Brak tekstu + + Brak tekstu utworu + Usuń playlistę - %1$s?]]> - Usuń listy odtwarzania + %1$s?]]> + + Usuń playlisty + Usuń utwór %1$s?]]> + Usuń utwory - %1$d ?]]> - %1$d ?]]> + + %1$d playlist?]]> + %1$d utworów?]]> Usunięto %1$d utworów. + Usuwanie utworów + Głębia + Opis + Informacje o urządzeniu + Pozwól Retro Music na modyfikację ustawień dźwięku + Ustaw jako dzwonek + Czy chcesz wyczyścić czarną listę? %1$s z czarnej listy?]]> + Wesprzyj nas + Jeżeli uważasz, że zasługuje na zapłatę za moją pracę możesz zostawić tu drobną sumę + Kup mi: + Pobierz z Last.fm + Tryb samochodowy + Edytuj + Edytuj okładkę + Pusto - Korektor dźwięku + + Equalizer + Błąd - Najczęściej zadawane pytania + + Często zadawane pytania + Ulubione - Dokończ ostatnią piosenkę - Dopasuj + + Dokończ ostatni utwór + + Dopasowany + Płaski + Foldery + Śledź system - Dla Ciebie + + Dla ciebie + Darmowe - Pełne + + Pełny + Pełna karta + Zmień motyw i kolory aplikacji Wygląd i zachowanie interfejsu + Gatunek + Gatunki + Zobacz kod na GitHubie + Dołącz do społeczności Google Plus by uzyskać informacje o aktualizacjach i pomoc + 1 2 3 @@ -183,368 +316,584 @@ 6 7 8 + Styl siatki + Zawias + Historia + Strona główna + Obrót poziomy - Obraz - Obraz gradientowy + + Zdjęcie + + Zdjęcie gradient + Zmień ustawienia pobierania obrazka wykonawcy - Dodano %1$d utworów do listy odtwarzania %2$s. + + Dodano %1$d utworów do playlisty %2$s. + + Instagram Udostępnij swój setup aplikacji Retro Music na Instagramie + Klawiatura - Przepływność - Typ + + Przepustowość + + Format Nazwa pliku - Ścieżka pliku + Ścieżka do pliku Rozmiar - Więcej z %s + + Więcej od %s + Częstotliwość próbkowania + Długość + Wszystkie podpisy + Ostatnio dodane + Ostatni utwór + Odtwórzmy jakąś muzykę + Biblioteka + Kategorie biblioteki + Licencje - Śnieżno biały + + Śnieżnobiały + Słuchacze + Listowanie plików - Ładowanie... + + Ładowanie… + Zaloguj się - Tekst utworu + + Teksty utworów + Stworzone z ❤️ w Indiach + Materialistyczny + Błąd + Błąd uprawnień - Moje imię + + Nazwa + Najczęściej odtwarzane + Nigdy + Nowe zdjęcie banera - Nowa lista odtwarzania + + Nowa playlista + Nowe zdjęcie profilowe - %s jest nowym katalogiem startowym. + + %s jest nowym domyślnym katalogiem. + Następny utwór + Brak albumów + Brak artystów + "Odtwórz jakiś utwór i spróbuj ponownie." - Nie znaleziono korektora dźwięku. + + Nie znaleziono equalizera + Brak gatunków - Nie znaleziono tekstu utworu + + Brak tekstu utworu + Brak odtwarzanych utworów - Brak list odtwarzania - Nie znaleziono zakupów. - Brak wyników. - Brak utworów - Normalne - Normalny tekst utworu - Normalny - %s nie znajduje się w magazynie multimediów.]]> - Nic do skanowania. - Nic do zobaczenia - Powiadomienie - Zmień wygląd powiadomień - Teraz odtwarzane - Kolejka teraz odtwarzanych - Dostosuj panel aktualnie odtwarzanych - 9+ motywów Teraz odtwarzane - Tylko przez Wi-Fi - Zaawansowane funkcje eksperymentalne - Inne ustawienia - Hasło - Przez 3 miesiące - Wklej tekst utworu tutaj - Szczyt - Odmowa dostępu do pamięci zewnętrznej. - Odmowa dostępu. - Personalizuj - Dostosuj interfejs \"Teraz odtwarzane\" - Wybierz z pamięci lokalnej - Wybierz obraz + + You have no playlists + + No purchase found. + + No results + + You have no songs + + Normal + + Normal lyrics + + Normal + + %s is not listed in the media store.]]> + + Nothing to scan. + Nothing to see + + Notification + + Customize the notification style + + Now playing + Now playing queue + Customize the now playing screen + 9+ now playing themes + + Only on Wi-Fi + + Advanced testing features + + Other + + Password + + Past 3 months + + Paste lyrics here + + Peak + + Permission to access external storage denied. + + Permissions denied. + + Personalize + + Customize your now playing and UI controls + + Pick from local storage + + Pick image + Pinterest - Śledź stronę Retro Music na Pintrest po więcej inspiracji - Wyraźny - Powiadomienie odtwarzania pokazuje przyciski play/pauza itp. - Powiadomienie odtwarzania - Pusta lista odtwarzania - Lista odtwarzania jest pusta - Nazwa listy odtwarzania - Listy odtwarzania - Styl detali albumu - Ilość rozmycia dla motywów, mniejsza ilość jest szybsza - Wartość rozmycia - Dostosuj rogi dolnego okna dialogowego - Narożniki okna dialogowego - Filtruj piosenki według długości - Filtruj długość utworów - Zaawansowane - Styl okładki - Dźwięk - Czarna lista - Przyciski kontrolne - Motyw - Obrazki - Biblioteka - Ekran blokady - Listy odtwarzania - Zatrzymuje odtwarzanie przy zerowym poziomie głośności i wznawia po jego zwiększeniu. Kiedy zwiększysz głośność, odtwarzanie rozpocznie się nawet gdy jesteś poza aplikacją - Zatrzymaj przy wyłączonym dźwięku - Ta opcja może mieć wpływ na zużycie baterii - Pozostaw ekran włączony - Kliknij aby otworzyć z lub przesunąć do przezroczystej nawigacji ekranu teraz odtwarzane - Kliknij albo przesuń - Efekt spadającego śniegu - Używaj okładki aktualnie odtwarzanego albumu jako tapety ekranu blokady - Zmniejsz głośność kiedy dostajesz powiadomienia - Zawartość czarnej listy jest ukryta w twojej bibliotece. - Rozpocznij odtwarzanie po podłączeniu urządzenia bluetooth - Rozmazuj okładkę albumu na ekranie blokady. Może powodować problemy z aplikacjami i widżetami innych firm - Efekt karuzeli na okładce obecnie odtwarzanego albumu. Zwróć uwagę, że motyw karty i rozmycia nie zadziała - Użyj klasycznego wyglądu powiadomień. - Tło oraz kolor przycisku sterującego zmieniają się zgodnie z okładką albumu na ekranie odtwarzacza - Koloruje skróty aplikacji w kolorze akcentującym. Za każdym razem, gdy zmieniasz kolor, włącz tę opcję, aby zmiany odniosły skutek. - Koloruje pasek nawigacji kolorem wiodącym. - "Koloruje powiadomienie dominuj\u0105cym kolorem ok\u0142adki albumu." - Zgodnie z zaleceniami Material Design, w trybie ciemnym kolory powinny być mniej nasycone - Najbardziej dominujący kolor zostanie wybrany z okładki albumu lub wykonawcy. - Dodaj ekstra przyciski do małego odtwarzacza - Pokaż dodatkowe informacje o utworze, takie jak format pliku, częstość próbkowania i częstotliwość - "Może powodować problemy z odtwarzaniem na niektórych urządzeniach." - Przełącz kartę gatunku - Przełącz styl banera strony głównej - Może zwiększyć jakość okładki albumu, ale powoduje spowolnienie ładowania obrazu. Włącz tę opcję tylko jeśli masz problemy z okładkami o niskiej rozdzielczości. - Skonfiguruj widoczność i kolejność kategorii w bibliotece. - Sterowanie Retro na zablokowanym ekranie. - Szczegóły licencji oprogramowania open source - Zaokrąglone krawędzie okna, okładek albumów itp. - Włącz/wyłącz tytuły kart u dołu. - Tryb imersji - Zacznij odtwarzanie po podłączanie zestawu słuchawkowego - Odtwarzanie losowe zostanie wyłączone podczas odtwarzania nowej listy utworów - Włącz sterowanie głośnością na ekranie \"teraz odtwarzane\" - Pokaż okładkę albumu - Wygląd okładki albumu - Przesuwanie okładki albumu - Siatka albumów - Kolorowe skróty aplikacji - Siatka wykonawców - Zmniejsz głośność przy braku skupienia - Automatycznie pobierz obrazki wykonawców - Czarna lista - Odtwarzanie przez Bluetooth - Rozmaż okładkę albumu - Wybierz korektor dźwięku - Klasyczny wygląd powiadomień - Kolor adaptacyjny - Kolorowe powiadomienia - Mniej nasycony kolor - Dodatkowe sterowanie - Informacje o utworze - Odtwarzanie bez przerw - Motyw główny - Pokaż kartę gatunku - Siatka wykonawców strony głównej - Baner strony głównej - Ignoruj okładki z Media Store - Ostatnio dodany interwał listy odtwarzania - Sterowanie pełno ekranowe - Kolorowy pasek nawigacji - Wygląd - Licencje Open Source - Narożne krawędzie - Wygląd tytułów kart - Efekt karuzeli - Kolor dominujący - Aplikacja pełnoekranowa - Tytuły kart - Autoodtwarzanie - Tryb losowy - Sterowanie głośnością - Informacje użytkownika - Kolor podstawowy - Główny kolor motywu, domyślnie niebieski, działa teraz z ciemnymi kolorami + Follow Pinterest page for Retro Music design inspiration + + Plain + + The playing notification provides actions for play/pause etc. + Playing notification + + Empty playlist + + Playlist is empty + + Playlist name + + Playlists + + Album detail style + + Amount of blur applied for blur themes, lower is faster + Blur amount + + Adjust the bottom sheet dialog corners + Dialog corner + + Filter songs by length + Filter song duration + + Advanced + Album style + Audio + Blacklist + Controls + Theme + Images + Library + Lockscreen + Playlists + + Pauses the song when the volume decreases to zero and starts playing back when the volume level rises. Also works outside the app + Pause on zero + Keep in mind that enabling this feature may affect battery life + Keep the screen on + + Click to open with or slide to without transparent navigation of now playing screen + Click or Slide + + Snow fall effect + + Show Album Artists in the Artist category + Use the currently playing song album cover as the lockscreen wallpaper + Lower the volume when a system sound is played or a notification is received + The content of blacklisted folders is hidden from your library. + Start playing as soon as connected to bluetooth device + Blur the album cover on the lockscreen. Can cause problems with third party apps and widgets + Carousel effect for the album art in the now playing screen. Note that Card and Blur Card themes won\'t work + Use the classic notification design + The background and control button colors change according to the album art from the now playing screen + Colors the app shortcuts in the accent color. Every time you change the color please toggle this to take effect + Colors the navigation bar in the primary color + "Colors the notification in the album cover\u2019s vibrant color" + As per Material Design guide lines in dark mode colors should be desaturated + Most dominant color will be picked from the album or artist cover + Add extra controls for mini player + Show extra Song information, such as file format, bitrate and frequency + "Can cause playback issues on some devices." + Toggle genre tab + Show or hide the home banner + Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks + Configure visibility and order of library categories. + Use Retro Music\'s custom lockscreen controls + License details for open source software + Round the app\'s edges + Toggle titles for the bottom navigation bar tabs + Immersive mode + Start playing immediately after headphones are connected + Shuffle mode will turn off when playing a new list of songs + If enough space is available, show volume controls in the now playing screen + + Navigate by Album Artist + Show album cover + Album cover theme + Album cover skip + Album grid + Colored app shortcuts + Artist grid + Reduce volume on focus loss + Auto-download artist images + Blacklist + Bluetooth playback + Blur album cover + Choose equalizer + Classic notification design + Adaptive color + Colored notification + Desaturated color + Extra controls + Song info + Gapless playback + App theme + Show genre tab + Artist grid + Album grid + Banner + Ignore Media Store covers + Last added playlist interval + Fullscreen controls + Colored navigation bar + Now playing theme + Open source licences + Corner edges + Tab titles mode + Carousel effect + Dominant color + Fullscreen app + Tab titles + Auto-play + Shuffle mode + Volume controls + User info + + Primary color + The primary theme color, defaults to blue grey, for now works with dark colors + Pro - Efekt karuzeli, kolorowe motywy i wiele więcej... - Profil - Kup - *Zastanów się przed zakupem, nie proś o zwrot. - Kolejka - Oceń aplikację - Uwielbiasz tą aplikację? Daj nam znać w sklepie Google Play co o niej sądzisz i co powinniśmy poprawić. - Ostatnie albumy - Ostatni artyści - Usuń - Usuń zdjęcie banera - Usuń okładkę - Usuń z czarnej listy - Usuń zdjęcie profilowe - Usuń utwór z listy odtwarzania - %1$s z listy odtwarzania?]]> - Usuń te utwory z listy odtwarzania - %1$d z listy odtwarzania?]]> - Zmień nazwę listy odtwarzania - Zgłoś problem - Zgłoś błąd - Zresetuj - Zresetuj obrazek wykonawcy - Przywróć - Przywrócono poprzedni zakup. Zrestartuj aplikacje aby korzystać z wszystkich funkcji. - Przywrócono poprzednie zakupy. - Przywracanie zakupu... - Korektor dźwięku Retro + + Black theme, Now playing themes, Carousel effect and more.. + + Profile + + Purchase + + *Think before buying, don\'t ask for refund. + + Playing Queue + + Rate the app + + Love this app? Let us know in the Google Play Store how we can make it even better + + Recent albums + + Recent artists + + Remove + + Remove banner photo + + Remove cover + + Remove from blacklist + + Remove profile photo + + Remove song from playlist + %1$s from the playlist?]]> + + Remove songs from playlist + + %1$d songs from the playlist?]]> + + Rename playlist + + Report an issue + + Report bug + + Reset + + Reset artist image + + Restore + + Restored previous purchase. Please restart the app to make use of all features. + Restored previous purchases. + + Restoring purchase… + + Retro Music Equalizer + Retro Music Player Retro Music Pro - Nie udało się usunąć pliku: %s + + File delete failed: %s + - Nie można uzyskać URI SAF - Otwórz szufladę nawigacji - Włącz \'Pokaż kartę SD\' w menu przeciążenia + Can\'t get SAF URI + + Open navigation drawer + Enable \'Show SD card\' in overflow menu - %s wymaga dostępu do karty SD - Musisz wybrać katalog główny karty SD - Wybierz kartę SD w panelu nawigacji - Nie otwieraj żadnych podfolderów - Dotknij przycisku \'Wybierz\' u dołu ekranu - Nie udało się zapisać do pliku: %s - Zapisz + %s needs SD card access + You need to select your SD card root directory + Select your SD card in navigation drawer + Do not open any sub-folders + Tap \'select\' button at the bottom of the screen + + File write failed: %s + + Save + - Zapisz jako plik - Zapisz jako - Zapisz listę odtwarzania do %s. - Zapisywanie zmian - Skanuj media - Zeskanowano %1$d z plików %2$d. - Scrobble - Przeszukaj bibliotekę... - Zaznacz wszystko - Wybierz zdjęcie banera - Zaznaczone - Zgłoś awarię - Ustaw - Ustaw obrazek wykonawcy - Ustaw zdjęcie profilowe - Udostępnij aplikację - Podziel się z opowieściami - Losowo - Prosty - Wyłącznik czasowy wyłączony. - Wyłącznik czasowy ustawiony na %d minut. - Slajd - Mały album - Społeczność - Udostępnij historię - Utwór - Długość utworu - Utwory - Porządek sortowania - Rosnąco + Save as file + + Save as files + + Saved playlist to %s. + + Saving changes + + Scan media + + Scanned %1$d of %2$d files. + + Scrobbles + + Search your library… + + Select all + + Select banner photo + + Selected + + Send crash log + + Set + + Set artist image + + Set a profile photo + + Share app + + Share to Stories + + Shuffle + + Simple + + Sleep timer canceled. + Sleep timer set for %d minutes from now. + + Slide + + Small album + + Social + + Share story + + Song + + Song duration + + Songs + + Sort order + Ascending Album - Artysta - Kompozytor - Dane - Data modyfikacji - Rok - Malejąco - Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego - Przeszukaj swoją bibliotekę - Stos - Rozpocznij odtwarzanie - Sugestie - Po prostu pokaż tylko swoje imię na ekranie głównym - Wspieraj rozwój - Przesuń, aby odblokować - Synchronizowany tekst utworu - Systemowy korektor dźwięku + Artist + Composer + Date added + Date modified + Year + Descending + + Sorry! Your device doesn\'t support speech input + + Search your library + + Stack + + Start playing music. + + Suggestions + + Just show your name on home screen + + Support development + + Swipe to unlock + + Synced lyrics + + System Equalizer + Telegram - Dołącz do grupy Telegram, aby zgłosić błędy, zasugerować zmiany i inne - Dziękuję! - Pliki dźwiękowy - Ten miesiąc - Ten tydzień - Ten rok - Mały - Tytuł - Ekran główny - Miłego popołudnia - Dzień dobry - Dobry wieczór - Dzień dobry - Dobry wieczór - Jak masz na imię? - Dzisiaj - Najczęściej odtwarzane albumy - Najczęściej odtwarzani artyści - "Ścieżka (2 dla ścieżki 2 lub 3004 dla ścieżki CD3 4)" - Numer utworu - Przetłumacz - Pomóż nam tłumaczyć aplikację na swój język - Strona na Twitterze - Podziel się swoim designem z Retro Music - Niepodpisane - Nie mo\u017cna odtworzy\u0107 utworu. - Następne - Zaktualizuj obrazek - Aktualizowanie... - Nazwa użytkownika - Wersja - Obrót pionowy - Wirtualizacja - Głośność - Przeszukaj sieć - Witaj, - Co chciałbyś udostępnić? - Co nowego - Okno - Zaokrąglone rogi - Ustaw %1$s jako dzwonek. - Wybrano %1$d - Rok - Musisz zaznaczyć przynajmniej jedną kategorię - Będziesz przekierowany do strony ze zgłoszeniami. - Informacje o twoim koncie są używane tylko do autoryzacji. - Ilość - Notatka(Opcjonalna) - Rozpocznij płatność - Wyświetl teraz odtwarzane - Kliknięcie powiadomienia pokaże teraz odtwarzane zamiast ekranu głównego - Mała karta - O %s - Wybierz język - Tłumacze - Osoby, które pomogły przetłumaczyć tę aplikację - Wypróbuj Retro Music Premium + Join the Telegram group to discuss bugs, make suggestions, show off and more + + Thank you! + + The audio file + + This month + + This week + + This year + + Tiny + + Title + + Dashboard + + Good afternoon + Good day + Good evening + Good morning + Good night + + What\'s Your Name + + Today + + Top albums + + Top artists + + "Track (2 for track 2 or 3004 for CD3 track 4)" + + Track number + + Translate + + Help us translate the app to your language + + Twitter + Share your design with Retro Music + + Unlabeled + + Couldn\u2019t play this song. + + Up next + + Update image + + Updating… + + Username + + Version + + Vertical flip + + Virtualizer + + Volume + + Web search + + Welcome, + + What do you want to share? + + What\'s New + + Window + + Rounded corners + + Set %1$s as your ringtone. + + %1$d selected + + Year + + You have to select at least one category. + + You will be forwarded to the issue tracker website. + + Your account data is only used for authentication. + Amount + Note(Optional) + Start payment + Show now playing screen + Clicking on the notification will show now playing screen instead of the home screen + + Tiny card + About %s + Select language + Translators + The people who helped translate this app + Try Retro Music Premium + Share the app with your friends and family + Need more help? + Gradient + User Name + Not recently played + Past 7 days + - Utwór - Utwory - Utwory - Utwory + Song + Songs Album - Albumy - Albumy - Albumy + Albums - %d Utwór - %d Utworu - %d Utworu - %d Utworu + %d Song + %d Songs %d Album - %d Albumu - %d Albumu - %d Albumu + %d Albums - %d Artysta - %d Artyści - %d Artyści - %d Artyści + %d Artist + %d Artists + Done + Import playlist + It imports all playlists listed in the Android Media Store with songs, if the playlists already exists, the songs will get merged. + Import + Song count + Ascending + Song count desc + The app needs permission to access your device storage for playing music + Storage Access + Ringtone + The app needs permission to access your device settings in order to set music as Ringtone diff --git a/app/src/main/res/values-pl-rPL/strings_placeholder.xml b/app/src/main/res/values-pl-rPL/strings_placeholder.xml new file mode 100644 index 000000000..46f655c70 --- /dev/null +++ b/app/src/main/res/values-pl-rPL/strings_placeholder.xml @@ -0,0 +1,550 @@ + + + Zespół, media społecznościowe + Kolor akcentu + Kolor akcentu motywu, domyślnie morski + O aplikacji + Dodaj do ulubionych + Dodaj do kolejki + Dodaj do playlisty + Wyczyść kolejkę + Wyczyść playlistę + Przełącz tryb powtarzania + Usuń + Usuń z urządzenia + Szczegóły + Przejdź do albumu + Przejdź do artysty + Idź do gatunku + Przejdź do katalogu startowego + Przyznaj + Rozmiar siatki + Rozmiar siatki (poziomo) + Nowa playlista + Następny + Odtwarzaj + Odtwórz wszystko + Odtwarzaj następne + Odtwarzanie/Pauza + Poprzedni + Usuń z ulubionych + Usuń z kolejki odtwarzania + Usuń z playlisty + Zmień nazwę + Zapisz kolejkę + Skanuj + Szukaj + Rozpocznij + Ustaw jako dzwonek + Ustaw jako katalog startowy + "Ustawienia" + Udostępnij + Losowo wszystko + Playlista losowo + Wyłącznik czasowy + Sortowanie + Edytor tagów + Przełącz na ulubione + Przełącz tryb losowy + Adaptacyjny + Dodaj + Dodaj tekst utworu + Dodaj\nzdjęcie + "Dodaj do playlisty" + Dodaj znaczniki czasowe do tekstu + "Dodano 1 tytuł do kolejki odtwarzania" + Dodano %1$d tytułów do kolejki + Album + Artysta albumu + Pole tytuł lub artysta jest puste. + Albumy + Zawsze + Hej sprawdź ten fajny odtwarzacz muzyki na: https://play.google.com/store/apps/details?id=%s + Losowo + Najczęściej odtwarzane utwory + Duży + Karta + Klasyczny + mały + Tekst + Artysta + Artyści + Odrzucono fokus dźwiękowy. + Dostosuj ustawienia dźwięku i equalizera + Automatyczny + Bazowy kolor motywu + Wzmocnienie Bassu + Biografia + Biografia + Po prostu czarny + Czarna lista + Rozmycie + Rozmyta Karta + Nie udało się wysłać raportu + Niepoprawny token dostępu. Proszę się skontaktować z twórcą aplikacji. + Problemy nie zostały włączone dla wybranego repozytorium. Proszę się skontaktować z twórcą aplikacji. + Wystąpił nieoczekiwany błąd. Proszę skontaktować się z twórcą aplikacji. + Zła nazwa użytkownika lub hasło + Problem + Wyślij ręcznie + Proszę opisać problemu + Proszę wpisać poprawne hasło GitHub + Proszę wpisać tytuł problemu + Proszę wpisać poprawną nazwę użytkownika GitHub + Wystąpił nieoczekiwany błąd. Wyczyść dane podręczne lub - jeśli błąd pojawi się ponownie - wyślij nam maila. + Wrzucanie raportu na GitHub... + Wyślij przez konto GitHub + Kup teraz + Anuluj + Karta + Okrągły + Kolorowa Karta + Karta + Karuzela + Efekt karuzeli na ekranie Teraz odtwarzane + Kaskadowy + Strumieniowanie + Lista zmian + Lista zmian zarządzana z aplikacji Telegram + Okręg + Okrągły + Klasyczny + Wyczyść + Wyczyść dane aplikacji + Wyczyść czarną listę + Wyczyść kolejkę + Wyczyść playlistę + %1$s? To nie może być cofnięte!]]> + Zamknij + Kolor + Kolor + Kolory + Kompozytor + Skopiowano informacje o urządzeniu do schowka + Nie mo\u017cna utworzy\u0107 playlisty. + "Nie mo\u017cna pobra\u0107 dopasowanej ok\u0142adki albumu" + Nie można przywrócić zakupów. + Nie można przeskanować %d plików. + Utwórz + Stworzono playlistę %1$s. + Członkowie i współpracownicy + Aktualnie odtwarzane %1$s wykonawcy %2$s. + Dość ciemny + Brak tekstu + Usuń playlistę + %1$s?]]> + Usuń listy odtwarzania + Usuń utwór + %1$s?]]> + Usuń utwory + %1$d ?]]> + %1$d ?]]> + Usunięto %1$d utworów. + Usuwanie utworów + Głębia + Opis + Informacje o urządzeniu + Pozwól Retro Music na modyfikację ustawień dźwięku + Ustaw jako dzwonek + Czy chcesz wyczyścić czarną listę? + %1$s z czarnej listy?]]> + Wesprzyj nas + Jeżeli uważasz, że zasługuje na zapłatę za moją pracę możesz zostawić tu drobną sumę + Kup mi: + Pobierz z Last.fm + Tryb samochodowy + Edytuj + Edytuj okładkę + Pusto + Korektor dźwięku + Błąd + Najczęściej zadawane pytania + Ulubione + Dokończ ostatnią piosenkę + Dopasuj + Płaski + Foldery + Śledź system + Dla Ciebie + Darmowe + Pełne + Pełna karta + Zmień motyw i kolory aplikacji + Wygląd i zachowanie interfejsu + Gatunek + Gatunki + Zobacz kod na GitHubie + Dołącz do społeczności Google Plus by uzyskać informacje o aktualizacjach i pomoc + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + Styl siatki + Zawias + Historia + Strona główna + Obrót poziomy + Obraz + Obraz gradientowy + Zmień ustawienia pobierania obrazka wykonawcy + Dodano %1$d utworów do listy odtwarzania %2$s. + Udostępnij swój setup aplikacji Retro Music na Instagramie + Klawiatura + Przepływność + Typ + Nazwa pliku + Ścieżka pliku + Rozmiar + Więcej z %s + Częstotliwość próbkowania + Długość + Wszystkie podpisy + Ostatnio dodane + Ostatni utwór + Odtwórzmy jakąś muzykę + Biblioteka + Kategorie biblioteki + Licencje + Śnieżno biały + Słuchacze + Listowanie plików + Ładowanie... + Zaloguj się + Tekst utworu + Stworzone z ❤️ w Indiach + Materialistyczny + Błąd + Błąd uprawnień + Moje imię + Najczęściej odtwarzane + Nigdy + Nowe zdjęcie banera + Nowa lista odtwarzania + Nowe zdjęcie profilowe + %s jest nowym katalogiem startowym. + Następny utwór + Brak albumów + Brak artystów + "Odtwórz jakiś utwór i spróbuj ponownie." + Nie znaleziono korektora dźwięku. + Brak gatunków + Nie znaleziono tekstu utworu + Brak odtwarzanych utworów + Brak list odtwarzania + Nie znaleziono zakupów. + Brak wyników. + Brak utworów + Normalne + Normalny tekst utworu + Normalny + %s nie znajduje się w magazynie multimediów.]]> + Nic do skanowania. + Nic do zobaczenia + Powiadomienie + Zmień wygląd powiadomień + Teraz odtwarzane + Kolejka teraz odtwarzanych + Dostosuj panel aktualnie odtwarzanych + 9+ motywów Teraz odtwarzane + Tylko przez Wi-Fi + Zaawansowane funkcje eksperymentalne + Inne ustawienia + Hasło + Przez 3 miesiące + Wklej tekst utworu tutaj + Szczyt + Odmowa dostępu do pamięci zewnętrznej. + Odmowa dostępu. + Personalizuj + Dostosuj interfejs \"Teraz odtwarzane\" + Wybierz z pamięci lokalnej + Wybierz obraz + Pinterest + Śledź stronę Retro Music na Pintrest po więcej inspiracji + Wyraźny + Powiadomienie odtwarzania pokazuje przyciski play/pauza itp. + Powiadomienie odtwarzania + Pusta lista odtwarzania + Lista odtwarzania jest pusta + Nazwa listy odtwarzania + Listy odtwarzania + Styl detali albumu + Ilość rozmycia dla motywów, mniejsza ilość jest szybsza + Wartość rozmycia + Dostosuj rogi dolnego okna dialogowego + Narożniki okna dialogowego + Filtruj piosenki według długości + Filtruj długość utworów + Zaawansowane + Styl okładki + Dźwięk + Czarna lista + Przyciski kontrolne + Motyw + Obrazki + Biblioteka + Ekran blokady + Listy odtwarzania + Zatrzymuje odtwarzanie przy zerowym poziomie głośności i wznawia po jego zwiększeniu. Kiedy zwiększysz głośność, odtwarzanie rozpocznie się nawet gdy jesteś poza aplikacją + Zatrzymaj przy wyłączonym dźwięku + Ta opcja może mieć wpływ na zużycie baterii + Pozostaw ekran włączony + Kliknij aby otworzyć z lub przesunąć do przezroczystej nawigacji ekranu teraz odtwarzane + Kliknij albo przesuń + Efekt spadającego śniegu + Używaj okładki aktualnie odtwarzanego albumu jako tapety ekranu blokady + Zmniejsz głośność kiedy dostajesz powiadomienia + Zawartość czarnej listy jest ukryta w twojej bibliotece. + Rozpocznij odtwarzanie po podłączeniu urządzenia bluetooth + Rozmazuj okładkę albumu na ekranie blokady. Może powodować problemy z aplikacjami i widżetami innych firm + Efekt karuzeli na okładce obecnie odtwarzanego albumu. Zwróć uwagę, że motyw karty i rozmycia nie zadziała + Użyj klasycznego wyglądu powiadomień. + Tło oraz kolor przycisku sterującego zmieniają się zgodnie z okładką albumu na ekranie odtwarzacza + Koloruje skróty aplikacji w kolorze akcentującym. Za każdym razem, gdy zmieniasz kolor, włącz tę opcję, aby zmiany odniosły skutek. + Koloruje pasek nawigacji kolorem wiodącym. + "Koloruje powiadomienie dominuj\u0105cym kolorem ok\u0142adki albumu." + Zgodnie z zaleceniami Material Design, w trybie ciemnym kolory powinny być mniej nasycone + Najbardziej dominujący kolor zostanie wybrany z okładki albumu lub wykonawcy. + Dodaj ekstra przyciski do małego odtwarzacza + Pokaż dodatkowe informacje o utworze, takie jak format pliku, częstość próbkowania i częstotliwość + "Może powodować problemy z odtwarzaniem na niektórych urządzeniach." + Przełącz kartę gatunku + Przełącz styl banera strony głównej + Może zwiększyć jakość okładki albumu, ale powoduje spowolnienie ładowania obrazu. Włącz tę opcję tylko jeśli masz problemy z okładkami o niskiej rozdzielczości. + Skonfiguruj widoczność i kolejność kategorii w bibliotece. + Sterowanie Retro na zablokowanym ekranie. + Szczegóły licencji oprogramowania open source + Zaokrąglone krawędzie okna, okładek albumów itp. + Włącz/wyłącz tytuły kart u dołu. + Tryb imersji + Zacznij odtwarzanie po podłączanie zestawu słuchawkowego + Odtwarzanie losowe zostanie wyłączone podczas odtwarzania nowej listy utworów + Włącz sterowanie głośnością na ekranie \"teraz odtwarzane\" + Pokaż okładkę albumu + Wygląd okładki albumu + Przesuwanie okładki albumu + Siatka albumów + Kolorowe skróty aplikacji + Siatka wykonawców + Zmniejsz głośność przy braku skupienia + Automatycznie pobierz obrazki wykonawców + Czarna lista + Odtwarzanie przez Bluetooth + Rozmaż okładkę albumu + Wybierz korektor dźwięku + Klasyczny wygląd powiadomień + Kolor adaptacyjny + Kolorowe powiadomienia + Mniej nasycony kolor + Dodatkowe sterowanie + Informacje o utworze + Odtwarzanie bez przerw + Motyw główny + Pokaż kartę gatunku + Siatka wykonawców strony głównej + Baner strony głównej + Ignoruj okładki z Media Store + Ostatnio dodany interwał listy odtwarzania + Sterowanie pełno ekranowe + Kolorowy pasek nawigacji + Wygląd + Licencje Open Source + Narożne krawędzie + Wygląd tytułów kart + Efekt karuzeli + Kolor dominujący + Aplikacja pełnoekranowa + Tytuły kart + Autoodtwarzanie + Tryb losowy + Sterowanie głośnością + Informacje użytkownika + Kolor podstawowy + Główny kolor motywu, domyślnie niebieski, działa teraz z ciemnymi kolorami + Pro + Efekt karuzeli, kolorowe motywy i wiele więcej... + Profil + Kup + *Zastanów się przed zakupem, nie proś o zwrot. + Kolejka + Oceń aplikację + Uwielbiasz tą aplikację? Daj nam znać w sklepie Google Play co o niej sądzisz i co powinniśmy poprawić. + Ostatnie albumy + Ostatni artyści + Usuń + Usuń zdjęcie banera + Usuń okładkę + Usuń z czarnej listy + Usuń zdjęcie profilowe + Usuń utwór z listy odtwarzania + %1$s z listy odtwarzania?]]> + Usuń te utwory z listy odtwarzania + %1$d z listy odtwarzania?]]> + Zmień nazwę listy odtwarzania + Zgłoś problem + Zgłoś błąd + Zresetuj + Zresetuj obrazek wykonawcy + Przywróć + Przywrócono poprzedni zakup. Zrestartuj aplikacje aby korzystać z wszystkich funkcji. + Przywrócono poprzednie zakupy. + Przywracanie zakupu... + Korektor dźwięku Retro + Retro Music Player + Retro Music Pro + Nie udało się usunąć pliku: %s + + Nie można uzyskać URI SAF + Otwórz szufladę nawigacji + Włącz \'Pokaż kartę SD\' w menu przeciążenia + + %s wymaga dostępu do karty SD + Musisz wybrać katalog główny karty SD + Wybierz kartę SD w panelu nawigacji + Nie otwieraj żadnych podfolderów + Dotknij przycisku \'Wybierz\' u dołu ekranu + Nie udało się zapisać do pliku: %s + Zapisz + + + Zapisz jako plik + Zapisz jako + Zapisz listę odtwarzania do %s. + Zapisywanie zmian + Skanuj media + Zeskanowano %1$d z plików %2$d. + Scrobble + Przeszukaj bibliotekę... + Zaznacz wszystko + Wybierz zdjęcie banera + Zaznaczone + Zgłoś awarię + Ustaw + Ustaw obrazek wykonawcy + Ustaw zdjęcie profilowe + Udostępnij aplikację + Podziel się z opowieściami + Losowo + Prosty + Wyłącznik czasowy wyłączony. + Wyłącznik czasowy ustawiony na %d minut. + Slajd + Mały album + Społeczność + Udostępnij historię + Utwór + Długość utworu + Utwory + Porządek sortowania + Rosnąco + Album + Artysta + Kompozytor + Dane + Data modyfikacji + Rok + Malejąco + Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego + Przeszukaj swoją bibliotekę + Stos + Rozpocznij odtwarzanie + Sugestie + Po prostu pokaż tylko swoje imię na ekranie głównym + Wspieraj rozwój + Przesuń, aby odblokować + Synchronizowany tekst utworu + Systemowy korektor dźwięku + + Telegram + Dołącz do grupy Telegram, aby zgłosić błędy, zasugerować zmiany i inne + Dziękuję! + Pliki dźwiękowy + Ten miesiąc + Ten tydzień + Ten rok + Mały + Tytuł + Ekran główny + Miłego popołudnia + Dzień dobry + Dobry wieczór + Dzień dobry + Dobry wieczór + Jak masz na imię? + Dzisiaj + Najczęściej odtwarzane albumy + Najczęściej odtwarzani artyści + "Ścieżka (2 dla ścieżki 2 lub 3004 dla ścieżki CD3 4)" + Numer utworu + Przetłumacz + Pomóż nam tłumaczyć aplikację na swój język + Strona na Twitterze + Podziel się swoim designem z Retro Music + Niepodpisane + Nie mo\u017cna odtworzy\u0107 utworu. + Następne + Zaktualizuj obrazek + Aktualizowanie... + Nazwa użytkownika + Wersja + Obrót pionowy + Wirtualizacja + Głośność + Przeszukaj sieć + Witaj, + Co chciałbyś udostępnić? + Co nowego + Okno + Zaokrąglone rogi + Ustaw %1$s jako dzwonek. + Wybrano %1$d + Rok + Musisz zaznaczyć przynajmniej jedną kategorię + Będziesz przekierowany do strony ze zgłoszeniami. + Informacje o twoim koncie są używane tylko do autoryzacji. + Ilość + Notatka(Opcjonalna) + Rozpocznij płatność + Wyświetl teraz odtwarzane + Kliknięcie powiadomienia pokaże teraz odtwarzane zamiast ekranu głównego + Mała karta + O %s + Wybierz język + Tłumacze + Osoby, które pomogły przetłumaczyć tę aplikację + Wypróbuj Retro Music Premium + + Utwór + Utwory + Utwory + Utwory + + + Album + Albumy + Albumy + Albumy + + + %d Utwór + %d Utworu + %d Utworu + %d Utworu + + + %d Album + %d Albumu + %d Albumu + %d Albumu + + + %d Artysta + %d Artyści + %d Artyści + %d Artyści + + From 962afbfc0ac84f6d09af75c1de2bb908e5bffb15 Mon Sep 17 00:00:00 2001 From: Lambada10 <62511588+Lambada10@users.noreply.github.com> Date: Mon, 12 Oct 2020 20:27:08 +0200 Subject: [PATCH 69/72] Update Polish translation v2 --- app/src/main/res/values-pl-rPL/strings.xml | 242 ++++++++++----------- 1 file changed, 121 insertions(+), 121 deletions(-) diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index b8efb1210..c04d8ec74 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -417,170 +417,170 @@ Brak odtwarzanych utworów - You have no playlists + Brak playlist - No purchase found. + Nie znaleziono zakupu. - No results + Brak wyników - You have no songs + Brak utworów - Normal + Normalnie - Normal lyrics + Normalny tekst utworu - Normal + Normalny - %s is not listed in the media store.]]> + %s nie znajduje się w magazynie multimediów.]]> - Nothing to scan. - Nothing to see + Nic do skanowania. + Nic do zobaczenia - Notification + Powiadomienie - Customize the notification style + Zmień wygląd powiadomienia - Now playing - Now playing queue - Customize the now playing screen - 9+ now playing themes + Teraz odtwarzane + Kolejka odtwarzania + Dostosuj ekran kolejki odtwarzania + 9+ motywów \"Teraz odtwarzane\" - Only on Wi-Fi + Tylko przez Wi-Fi - Advanced testing features + Zaawansowane funkcje eksperymentalne - Other + Inne - Password + Hasło - Past 3 months + Ostatnie 3 miesiące - Paste lyrics here + Wklej tekst utworu tutaj - Peak + Szczyt - Permission to access external storage denied. + Odmowa dostępu do pamięci zewnętrznej. - Permissions denied. + Odmowa dostępu. - Personalize + Personalizuj - Customize your now playing and UI controls + Dostosuj interfejs \"Teraz odtwarzane\" - Pick from local storage + Wybierz z pamięci urządzenia - Pick image + Wybierz zdjęcie Pinterest - Follow Pinterest page for Retro Music design inspiration + Śledź stronę Retro Music na Pintrest po więcej inspiracji - Plain + Wyraźny - The playing notification provides actions for play/pause etc. - Playing notification + Powiadomienie odtwarzania pokazuje przyciski play/pauza itp. + Powiadomienie odtwarzania - Empty playlist + Wyczyść playlistę - Playlist is empty + Playlista jest pusta - Playlist name + Nazwa playlisty - Playlists + Playlisty - Album detail style + Styl informacji o albumie - Amount of blur applied for blur themes, lower is faster - Blur amount + Ilość rozmycia dla motywów, mniejsza ilość jest szybsza + Ilość rozmycia - Adjust the bottom sheet dialog corners - Dialog corner + Dostosuj rogi dolnego okna dialogowego + Narożniki okna dialogowego - Filter songs by length - Filter song duration + Filtruj utwory według długości + Filtruj długość utworów - Advanced - Album style + Zaawansowane + Styl albumów Audio - Blacklist - Controls - Theme - Images - Library - Lockscreen - Playlists + Czarna lista + Przełączniki + Motyw + Obrazki + Biblioteka + Ekran blokady + Playlisty - Pauses the song when the volume decreases to zero and starts playing back when the volume level rises. Also works outside the app - Pause on zero - Keep in mind that enabling this feature may affect battery life - Keep the screen on + Zatrzymuje odtwarzanie przy zerowym poziomie głośności i wznawia po jego zwiększeniu. Kiedy zwiększysz głośność, odtwarzanie rozpocznie się nawet gdy jesteś poza aplikacją + Zatrzymaj przy wyłączonym dźwięku + Ta opcja może mieć wpływ na zużycie baterii + Pozostaw ekran włączony - Click to open with or slide to without transparent navigation of now playing screen - Click or Slide + Kliknij aby otworzyć z lub przesunąć do przezroczystej nawigacji ekranu teraz odtwarzane + Kliknij albo przesuń - Snow fall effect + Efekt spadającego śniegu - Show Album Artists in the Artist category - Use the currently playing song album cover as the lockscreen wallpaper - Lower the volume when a system sound is played or a notification is received - The content of blacklisted folders is hidden from your library. - Start playing as soon as connected to bluetooth device - Blur the album cover on the lockscreen. Can cause problems with third party apps and widgets - Carousel effect for the album art in the now playing screen. Note that Card and Blur Card themes won\'t work - Use the classic notification design - The background and control button colors change according to the album art from the now playing screen - Colors the app shortcuts in the accent color. Every time you change the color please toggle this to take effect - Colors the navigation bar in the primary color - "Colors the notification in the album cover\u2019s vibrant color" - As per Material Design guide lines in dark mode colors should be desaturated - Most dominant color will be picked from the album or artist cover - Add extra controls for mini player - Show extra Song information, such as file format, bitrate and frequency - "Can cause playback issues on some devices." - Toggle genre tab - Show or hide the home banner - Can increase the album cover quality, but causes slower image loading times. Only enable this if you have problems with low resolution artworks - Configure visibility and order of library categories. - Use Retro Music\'s custom lockscreen controls - License details for open source software - Round the app\'s edges - Toggle titles for the bottom navigation bar tabs - Immersive mode - Start playing immediately after headphones are connected - Shuffle mode will turn off when playing a new list of songs - If enough space is available, show volume controls in the now playing screen + Pokazuj artystów albumu w kategorii \"Artyści\" + Używaj okładki aktualnie odtwarzanego albumu jako tapety ekranu blokady + Zmniejsz głośność kiedy dostajesz powiadomienia + Zawartość czarnej listy jest ukryta w twojej bibliotece. + Rozpocznij odtwarzanie po podłączeniu urządzenia bluetooth + Rozmazuj okładkę albumu na ekranie blokady. Może powodować problemy z aplikacjami i widżetami innych firm + Efekt karuzeli na okładce obecnie odtwarzanego albumu. Zwróć uwagę, że motyw karty i rozmycia nie zadziałają + Użyj klasycznego wyglądu powiadomień + Tło oraz kolor przycisku sterującego zmieniają się zgodnie z okładką albumu na ekranie odtwarzacza + Koloruje skróty aplikacji w kolorze akcentującym. Za każdym razem, gdy zmieniasz kolor, włącz tę opcję, aby zmiany odniosły skutek + Koloruje pasek nawigacji kolorem wiodącym + "Koloruje powiadomienie dominującym kolorem okładki albumu" + Zgodnie z zaleceniami Material Design, w trybie ciemnym kolory powinny być mniej nasycone + Najbardziej dominujący kolor zostanie wybrany z okładki albumu lub wykonawcy + Dodaj dodatkowe przyciski do małego odtwarzacza + Pokaż dodatkowe informacje o utworze, takie jak format pliku, częstość próbkowania i częstotliwość + "Może powodować problemy z odtwarzaniem na niektórych urządzeniach." + Przełącz kartę gatunku + Pokaż lub ukryj baner strony głównej + Może zwiększyć jakość okładki albumu, ale powoduje spowolnienie ładowania obrazu. Włącz tę opcję tylko jeśli masz problemy z okładkami o niskiej rozdzielczości + Skonfiguruj widoczność i kolejność kategorii w bibliotece. + Sterowanie Retro na zablokowanym ekranie + Szczegóły licencji oprogramowania open source + Zaokrągl rogi w aplikacji + Przełącz tytuły kart u dołu + Tryb immersji + Zacznij odtwarzanie po podłączanie zestawu słuchawkowego + Odtwarzanie losowe zostanie wyłączone podczas odtwarzania nowej listy utworów + Pokaż sterowanie głośnością na ekranie \"teraz odtwarzane\" - Navigate by Album Artist - Show album cover - Album cover theme - Album cover skip - Album grid - Colored app shortcuts - Artist grid - Reduce volume on focus loss - Auto-download artist images - Blacklist - Bluetooth playback - Blur album cover - Choose equalizer - Classic notification design - Adaptive color - Colored notification - Desaturated color - Extra controls - Song info - Gapless playback - App theme - Show genre tab - Artist grid - Album grid - Banner - Ignore Media Store covers - Last added playlist interval - Fullscreen controls - Colored navigation bar - Now playing theme - Open source licences - Corner edges + Nawiguj po artystach albumów + Pokaż okładkę albumu + Wygląd okładki albumu + Przesuwanie okładki albumu + Siatka albumów + Kolorowe skróty aplikacji + Siatka artystów + Zmniejsz głośność przy braku skupienia + Automatycznie pobierz obrazki wykonawców + Czarna lista + Odtwarzanie przez Bluetooth + Rozmaż okładkę albumu + Wybierz equalizer + Klasyczny wygląd powiadomień + Kolor adaptacyjny + Kolorowe powiadomienia + Mniej nasycony kolor + Dodatkowe sterowanie + Informacje o utworze + Odtwarzanie bez przerw + Motyw aplikacji + Pokaż kartę gatunku + Siatka artystów + Siatka albumów + Baner + Ignoruj okładki z Media Store + Ostatnio dodany interwał listy odtwarzania + Sterowanie pełno ekranowe + Kolorowy pasek nawigacji + Wygląd \"Teraz odtwarzane\" + Licencje open source + Narożne krawędzie Tab titles mode Carousel effect Dominant color From 525e94c011765f379ffaad8a573bfcf1ebe51ef6 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 13 Oct 2020 11:45:05 +0300 Subject: [PATCH 70/72] Updated permission string fixes Updated permission string fixes and some other fixes --- app/src/main/res/values-ru-rRU/strings.xml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 8f08c5905..dd0fd6c63 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -8,13 +8,13 @@ О программe Добавить в избранное - Добавить в очередь проигрывания + Добавить в очередь воспроизведения Добавить в плейлист - Очистить очередь проигрывания + Очистить очередь воспроизведения Очистить плейлист - Режим повтора цикла + Режим циклического повтора Удалить Удалить с устройства @@ -58,7 +58,7 @@ Задать в качества рингтона Установить как стартовый каталог - "Настройки" + Настройки Поделиться @@ -102,7 +102,7 @@ Всегда - Эй, попробуй этот крутой музыкальный плеер на: https://play.google.com/store/apps/details?id=%s + Эй, попробуй этот крутой музыкальный плеер на Android: https://play.google.com/store/apps/details?id=%s Перемешать Лучшие треки @@ -519,6 +519,7 @@ Эффект снегопада + Только альбомы исполнителей Использовать обложку альбома текущей песни в качестве обоев на экране блокировки. Снизить громкость воспроизведения когда приходит звуковое уведомление Содержимое черного списка скрыто из вашей библиотеки. @@ -852,7 +853,7 @@ Нажатие на уведомление будет показывать экран воспроизведения вместо домашнего экрана Крошечная карточка - О программе %s + Об альбоме %s Выберите язык Переводчики Люди которые помогали переводить это приложение From 58decfb0b51739ac3302bdb3f856f9a3147659b0 Mon Sep 17 00:00:00 2001 From: Lambada10 <62511588+Lambada10@users.noreply.github.com> Date: Tue, 13 Oct 2020 20:28:22 +0200 Subject: [PATCH 71/72] Update Polish translation v3 --- app/src/main/res/values-pl-rPL/strings.xml | 346 ++++++++++----------- 1 file changed, 173 insertions(+), 173 deletions(-) diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index c04d8ec74..0356f3b58 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -581,319 +581,319 @@ Wygląd \"Teraz odtwarzane\" Licencje open source Narożne krawędzie - Tab titles mode - Carousel effect - Dominant color - Fullscreen app - Tab titles - Auto-play - Shuffle mode - Volume controls - User info + Wygląd tytułów kart + Efekt karuzeli + Kolor dominujący + Aplikacja pełnoekranowa + Tytuły kart + Autoodtwarzanie + Tryb losowy + Sterowanie głośnością + Informacje użytkownika - Primary color - The primary theme color, defaults to blue grey, for now works with dark colors + Kolor podstawowy + Główny kolor motywu, domyślnie niebieski, działa teraz z ciemnymi kolorami Pro - Black theme, Now playing themes, Carousel effect and more.. + Efekt karuzeli, kolorowe motywy i wiele więcej.. - Profile + Profil - Purchase + Kup - *Think before buying, don\'t ask for refund. + *Zastanów się przed zakupem, nie proś o zwrot. - Playing Queue + Kolejka odtwarzania - Rate the app + Oceń aplikację - Love this app? Let us know in the Google Play Store how we can make it even better + Uwielbiasz tą aplikację? Daj nam znać w sklepie Google Play co o niej sądzisz i co powinniśmy poprawić - Recent albums + Ostatnie albumy - Recent artists + Ostatni artyści - Remove + Usuń - Remove banner photo + Usuń zdjęcie banera - Remove cover + Usuń okładkę - Remove from blacklist + Usuń z czarnej listy - Remove profile photo + Usuń zdjęcie profilowe - Remove song from playlist - %1$s from the playlist?]]> + Usuń utwór z listy odtwarzania + %1$s z playlisty?]]> - Remove songs from playlist + Usuń te utwory z listy odtwarzania - %1$d songs from the playlist?]]> + %1$d utworów z playlisty?]]> - Rename playlist + Zmień nazwę playlisty - Report an issue + Zgłoś problem - Report bug + Zgłoś błąd - Reset + Zresetuj - Reset artist image + Zresetuj obrazek wykonawcy - Restore + Przywróć - Restored previous purchase. Please restart the app to make use of all features. - Restored previous purchases. + Przywrócono poprzedni zakup. Zrestartuj aplikacje aby korzystać z wszystkich funkcji. + Przywrócono poprzednie zakupy. - Restoring purchase… + Przywracanie zakupu… Retro Music Equalizer Retro Music Player Retro Music Pro - File delete failed: %s + Nie udało się usunąć pliku: %s - Can\'t get SAF URI + Nie można uzyskać URI SAF - Open navigation drawer - Enable \'Show SD card\' in overflow menu + Otwórz szufladę nawigacji + Włącz \'Pokaż kartę SD\' w menu - %s needs SD card access - You need to select your SD card root directory - Select your SD card in navigation drawer - Do not open any sub-folders - Tap \'select\' button at the bottom of the screen + %s wymaga dostępu do karty SD + Musisz wybrać katalog główny karty SD + Wybierz kartę SD w panelu nawigacji + Nie otwieraj żadnych podfolderów + Dotknij przycisku \'Wybierz\' u dołu ekranu - File write failed: %s + Nie udało się zapisać do pliku: %s - Save + Zapisz - Save as file + Zapisz jako plik - Save as files + Zapisz jako pliki - Saved playlist to %s. + Zapisz listę odtwarzania do %s. - Saving changes + Zapisywanie zmian - Scan media + Skanuj media - Scanned %1$d of %2$d files. + Zeskanowano %1$d z %2$d plików. - Scrobbles + Scrobble - Search your library… + Przeszukaj bibliotekę… - Select all + Zaznacz wszystko - Select banner photo + Wybierz zdjęcie banera - Selected + Zaznaczono - Send crash log + Zgłoś awarię - Set + Ustaw - Set artist image + Ustaw obrazek wykonawcy - Set a profile photo + Ustaw zdjęcie profilowe - Share app + Udostępnij aplikację - Share to Stories + Udostępnij na Stories - Shuffle + Losowo - Simple + Prosty - Sleep timer canceled. - Sleep timer set for %d minutes from now. + Wyłącznik czasowy anulowany. + Wyłącznik czasowy ustawiony na %d minut. - Slide + Ślizg - Small album + Mały album - Social + Społeczność - Share story + Udostępnij na Story - Song + Utwór - Song duration + Długość utworu - Songs + Utwory - Sort order - Ascending + Sortuj według + Rosnąco Album - Artist - Composer - Date added - Date modified - Year - Descending + Artysta + Kompozytor + Data dodania + Data modyfikacji + Rok + Malejąco - Sorry! Your device doesn\'t support speech input + Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego - Search your library + Przeszukaj swoją bibliotekę - Stack + Stos - Start playing music. + Rozpocznij odtwarzanie. - Suggestions + Sugestie - Just show your name on home screen + Po prostu pokażę twoje imię na ekranie głównym - Support development + Wspieraj rozwój - Swipe to unlock + Przesuń by odblokować - Synced lyrics + Synchronizowany tekst utworu - System Equalizer + Systemowy equalizer Telegram - Join the Telegram group to discuss bugs, make suggestions, show off and more + Dołącz do grupy Telegramie, aby zgłosić błędy, zasugerować zmiany i inne - Thank you! + Dziękuję! - The audio file + Plik dźwiękowy - This month + Ten miesiąc - This week + Ten tydzień - This year + Ten rok - Tiny + Mały - Title + Tytuł - Dashboard + Ekran główny - Good afternoon - Good day - Good evening - Good morning - Good night + Miłego popołudnia + Dzień dobry + Dobry wieczór + Dzień dobry + Dobry wieczór - What\'s Your Name + Jak masz na imię? - Today + Dziś - Top albums + Najczęściej odtwarzane albumy - Top artists + Najczęściej odtwarzani artyści - "Track (2 for track 2 or 3004 for CD3 track 4)" + "Ścieżka (2 dla ścieżki 2 lub 3004 dla ścieżki CD3 4)" - Track number + Numer utworu - Translate + Przetłumacz - Help us translate the app to your language + Pomóż nam tłumaczyć aplikację na swój język Twitter - Share your design with Retro Music + Podziel się swoim designem z Retro Music - Unlabeled + Niepodpisane - Couldn\u2019t play this song. + Nie można odtworzyć utworu. - Up next + Następne - Update image + Zaktualizuj obrazek - Updating… + Aktualizowanie… - Username + Nazwa użytkownika - Version + Wersja - Vertical flip + Obrót pionowy - Virtualizer + Wirtualizator - Volume + Głośność - Web search + Przeszukaj sieć - Welcome, + Witaj, - What do you want to share? + Co chciałbyś udostępnić? - What\'s New + Co nowego - Window + Okno - Rounded corners + Zaokrąglone rogi - Set %1$s as your ringtone. + Ustaw %1$s jako dzwonek. - %1$d selected + Wybrano %1$d - Year + Rok - You have to select at least one category. + Musisz zaznaczyć przynajmniej jedną kategorię. - You will be forwarded to the issue tracker website. + Będziesz przekierowany do strony ze zgłoszeniami. - Your account data is only used for authentication. - Amount - Note(Optional) - Start payment - Show now playing screen - Clicking on the notification will show now playing screen instead of the home screen + Informacje o twoim koncie są używane tylko do autoryzacji. + Ilość + Notatka(Opcjonalna) + Rozpocznij płatność + Wyświetl teraz odtwarzane + Kliknięcie powiadomienia pokaże teraz odtwarzane zamiast ekranu głównego - Tiny card - About %s - Select language - Translators - The people who helped translate this app - Try Retro Music Premium - Share the app with your friends and family - Need more help? + Mała karta + O %s + Wybierz język + Tłumacze + Osoby, które pomogły przetłumaczyć tę aplikację + Wypróbuj Retro Music Premium + Podziel się aplikacją z rodziną i przyjaciółmi + Potrzebujesz pomocy? Gradient - User Name - Not recently played - Past 7 days + Nazwa użytkownika + Nie odtwarzane ostatnio + Ostatnie 7 dni - Song - Songs + Utwór + Utwory Album - Albums + Albumy - %d Song - %d Songs + %d utwór + %d utwory %d Album - %d Albums + %d albumy - %d Artist - %d Artists + %d artysta + %d artyści - Done - Import playlist - It imports all playlists listed in the Android Media Store with songs, if the playlists already exists, the songs will get merged. + Gotowe + Zaimportuj playlisty + Importuje wszystkie playlisty listowane w Media Store z utworami, jeśli playlista istnieje, utwory zostaną połączone. Import - Song count - Ascending + Liczba utworów + Rosnąco Song count desc - The app needs permission to access your device storage for playing music - Storage Access - Ringtone - The app needs permission to access your device settings in order to set music as Ringtone + Aplikacja potrzebuje uprawnienia na dostęp do pamięci urządzenia aby odtwarzać muzykę + Dostęp do pamięci + Dzwonek + Aplikacja potrezbuje uprawnienia na dostęp do ustawień telefonu, aby ustawić utwór jako dzwonek telefonu From ceffbd976f44e81e38a5cc74a90df44b3b563964 Mon Sep 17 00:00:00 2001 From: Lambada10 <62511588+Lambada10@users.noreply.github.com> Date: Tue, 13 Oct 2020 20:29:18 +0200 Subject: [PATCH 72/72] Delete placeholder strings --- .../res/values-pl-rPL/strings_placeholder.xml | 550 ------------------ 1 file changed, 550 deletions(-) delete mode 100644 app/src/main/res/values-pl-rPL/strings_placeholder.xml diff --git a/app/src/main/res/values-pl-rPL/strings_placeholder.xml b/app/src/main/res/values-pl-rPL/strings_placeholder.xml deleted file mode 100644 index 46f655c70..000000000 --- a/app/src/main/res/values-pl-rPL/strings_placeholder.xml +++ /dev/null @@ -1,550 +0,0 @@ - - - Zespół, media społecznościowe - Kolor akcentu - Kolor akcentu motywu, domyślnie morski - O aplikacji - Dodaj do ulubionych - Dodaj do kolejki - Dodaj do playlisty - Wyczyść kolejkę - Wyczyść playlistę - Przełącz tryb powtarzania - Usuń - Usuń z urządzenia - Szczegóły - Przejdź do albumu - Przejdź do artysty - Idź do gatunku - Przejdź do katalogu startowego - Przyznaj - Rozmiar siatki - Rozmiar siatki (poziomo) - Nowa playlista - Następny - Odtwarzaj - Odtwórz wszystko - Odtwarzaj następne - Odtwarzanie/Pauza - Poprzedni - Usuń z ulubionych - Usuń z kolejki odtwarzania - Usuń z playlisty - Zmień nazwę - Zapisz kolejkę - Skanuj - Szukaj - Rozpocznij - Ustaw jako dzwonek - Ustaw jako katalog startowy - "Ustawienia" - Udostępnij - Losowo wszystko - Playlista losowo - Wyłącznik czasowy - Sortowanie - Edytor tagów - Przełącz na ulubione - Przełącz tryb losowy - Adaptacyjny - Dodaj - Dodaj tekst utworu - Dodaj\nzdjęcie - "Dodaj do playlisty" - Dodaj znaczniki czasowe do tekstu - "Dodano 1 tytuł do kolejki odtwarzania" - Dodano %1$d tytułów do kolejki - Album - Artysta albumu - Pole tytuł lub artysta jest puste. - Albumy - Zawsze - Hej sprawdź ten fajny odtwarzacz muzyki na: https://play.google.com/store/apps/details?id=%s - Losowo - Najczęściej odtwarzane utwory - Duży - Karta - Klasyczny - mały - Tekst - Artysta - Artyści - Odrzucono fokus dźwiękowy. - Dostosuj ustawienia dźwięku i equalizera - Automatyczny - Bazowy kolor motywu - Wzmocnienie Bassu - Biografia - Biografia - Po prostu czarny - Czarna lista - Rozmycie - Rozmyta Karta - Nie udało się wysłać raportu - Niepoprawny token dostępu. Proszę się skontaktować z twórcą aplikacji. - Problemy nie zostały włączone dla wybranego repozytorium. Proszę się skontaktować z twórcą aplikacji. - Wystąpił nieoczekiwany błąd. Proszę skontaktować się z twórcą aplikacji. - Zła nazwa użytkownika lub hasło - Problem - Wyślij ręcznie - Proszę opisać problemu - Proszę wpisać poprawne hasło GitHub - Proszę wpisać tytuł problemu - Proszę wpisać poprawną nazwę użytkownika GitHub - Wystąpił nieoczekiwany błąd. Wyczyść dane podręczne lub - jeśli błąd pojawi się ponownie - wyślij nam maila. - Wrzucanie raportu na GitHub... - Wyślij przez konto GitHub - Kup teraz - Anuluj - Karta - Okrągły - Kolorowa Karta - Karta - Karuzela - Efekt karuzeli na ekranie Teraz odtwarzane - Kaskadowy - Strumieniowanie - Lista zmian - Lista zmian zarządzana z aplikacji Telegram - Okręg - Okrągły - Klasyczny - Wyczyść - Wyczyść dane aplikacji - Wyczyść czarną listę - Wyczyść kolejkę - Wyczyść playlistę - %1$s? To nie może być cofnięte!]]> - Zamknij - Kolor - Kolor - Kolory - Kompozytor - Skopiowano informacje o urządzeniu do schowka - Nie mo\u017cna utworzy\u0107 playlisty. - "Nie mo\u017cna pobra\u0107 dopasowanej ok\u0142adki albumu" - Nie można przywrócić zakupów. - Nie można przeskanować %d plików. - Utwórz - Stworzono playlistę %1$s. - Członkowie i współpracownicy - Aktualnie odtwarzane %1$s wykonawcy %2$s. - Dość ciemny - Brak tekstu - Usuń playlistę - %1$s?]]> - Usuń listy odtwarzania - Usuń utwór - %1$s?]]> - Usuń utwory - %1$d ?]]> - %1$d ?]]> - Usunięto %1$d utworów. - Usuwanie utworów - Głębia - Opis - Informacje o urządzeniu - Pozwól Retro Music na modyfikację ustawień dźwięku - Ustaw jako dzwonek - Czy chcesz wyczyścić czarną listę? - %1$s z czarnej listy?]]> - Wesprzyj nas - Jeżeli uważasz, że zasługuje na zapłatę za moją pracę możesz zostawić tu drobną sumę - Kup mi: - Pobierz z Last.fm - Tryb samochodowy - Edytuj - Edytuj okładkę - Pusto - Korektor dźwięku - Błąd - Najczęściej zadawane pytania - Ulubione - Dokończ ostatnią piosenkę - Dopasuj - Płaski - Foldery - Śledź system - Dla Ciebie - Darmowe - Pełne - Pełna karta - Zmień motyw i kolory aplikacji - Wygląd i zachowanie interfejsu - Gatunek - Gatunki - Zobacz kod na GitHubie - Dołącz do społeczności Google Plus by uzyskać informacje o aktualizacjach i pomoc - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - Styl siatki - Zawias - Historia - Strona główna - Obrót poziomy - Obraz - Obraz gradientowy - Zmień ustawienia pobierania obrazka wykonawcy - Dodano %1$d utworów do listy odtwarzania %2$s. - Udostępnij swój setup aplikacji Retro Music na Instagramie - Klawiatura - Przepływność - Typ - Nazwa pliku - Ścieżka pliku - Rozmiar - Więcej z %s - Częstotliwość próbkowania - Długość - Wszystkie podpisy - Ostatnio dodane - Ostatni utwór - Odtwórzmy jakąś muzykę - Biblioteka - Kategorie biblioteki - Licencje - Śnieżno biały - Słuchacze - Listowanie plików - Ładowanie... - Zaloguj się - Tekst utworu - Stworzone z ❤️ w Indiach - Materialistyczny - Błąd - Błąd uprawnień - Moje imię - Najczęściej odtwarzane - Nigdy - Nowe zdjęcie banera - Nowa lista odtwarzania - Nowe zdjęcie profilowe - %s jest nowym katalogiem startowym. - Następny utwór - Brak albumów - Brak artystów - "Odtwórz jakiś utwór i spróbuj ponownie." - Nie znaleziono korektora dźwięku. - Brak gatunków - Nie znaleziono tekstu utworu - Brak odtwarzanych utworów - Brak list odtwarzania - Nie znaleziono zakupów. - Brak wyników. - Brak utworów - Normalne - Normalny tekst utworu - Normalny - %s nie znajduje się w magazynie multimediów.]]> - Nic do skanowania. - Nic do zobaczenia - Powiadomienie - Zmień wygląd powiadomień - Teraz odtwarzane - Kolejka teraz odtwarzanych - Dostosuj panel aktualnie odtwarzanych - 9+ motywów Teraz odtwarzane - Tylko przez Wi-Fi - Zaawansowane funkcje eksperymentalne - Inne ustawienia - Hasło - Przez 3 miesiące - Wklej tekst utworu tutaj - Szczyt - Odmowa dostępu do pamięci zewnętrznej. - Odmowa dostępu. - Personalizuj - Dostosuj interfejs \"Teraz odtwarzane\" - Wybierz z pamięci lokalnej - Wybierz obraz - Pinterest - Śledź stronę Retro Music na Pintrest po więcej inspiracji - Wyraźny - Powiadomienie odtwarzania pokazuje przyciski play/pauza itp. - Powiadomienie odtwarzania - Pusta lista odtwarzania - Lista odtwarzania jest pusta - Nazwa listy odtwarzania - Listy odtwarzania - Styl detali albumu - Ilość rozmycia dla motywów, mniejsza ilość jest szybsza - Wartość rozmycia - Dostosuj rogi dolnego okna dialogowego - Narożniki okna dialogowego - Filtruj piosenki według długości - Filtruj długość utworów - Zaawansowane - Styl okładki - Dźwięk - Czarna lista - Przyciski kontrolne - Motyw - Obrazki - Biblioteka - Ekran blokady - Listy odtwarzania - Zatrzymuje odtwarzanie przy zerowym poziomie głośności i wznawia po jego zwiększeniu. Kiedy zwiększysz głośność, odtwarzanie rozpocznie się nawet gdy jesteś poza aplikacją - Zatrzymaj przy wyłączonym dźwięku - Ta opcja może mieć wpływ na zużycie baterii - Pozostaw ekran włączony - Kliknij aby otworzyć z lub przesunąć do przezroczystej nawigacji ekranu teraz odtwarzane - Kliknij albo przesuń - Efekt spadającego śniegu - Używaj okładki aktualnie odtwarzanego albumu jako tapety ekranu blokady - Zmniejsz głośność kiedy dostajesz powiadomienia - Zawartość czarnej listy jest ukryta w twojej bibliotece. - Rozpocznij odtwarzanie po podłączeniu urządzenia bluetooth - Rozmazuj okładkę albumu na ekranie blokady. Może powodować problemy z aplikacjami i widżetami innych firm - Efekt karuzeli na okładce obecnie odtwarzanego albumu. Zwróć uwagę, że motyw karty i rozmycia nie zadziała - Użyj klasycznego wyglądu powiadomień. - Tło oraz kolor przycisku sterującego zmieniają się zgodnie z okładką albumu na ekranie odtwarzacza - Koloruje skróty aplikacji w kolorze akcentującym. Za każdym razem, gdy zmieniasz kolor, włącz tę opcję, aby zmiany odniosły skutek. - Koloruje pasek nawigacji kolorem wiodącym. - "Koloruje powiadomienie dominuj\u0105cym kolorem ok\u0142adki albumu." - Zgodnie z zaleceniami Material Design, w trybie ciemnym kolory powinny być mniej nasycone - Najbardziej dominujący kolor zostanie wybrany z okładki albumu lub wykonawcy. - Dodaj ekstra przyciski do małego odtwarzacza - Pokaż dodatkowe informacje o utworze, takie jak format pliku, częstość próbkowania i częstotliwość - "Może powodować problemy z odtwarzaniem na niektórych urządzeniach." - Przełącz kartę gatunku - Przełącz styl banera strony głównej - Może zwiększyć jakość okładki albumu, ale powoduje spowolnienie ładowania obrazu. Włącz tę opcję tylko jeśli masz problemy z okładkami o niskiej rozdzielczości. - Skonfiguruj widoczność i kolejność kategorii w bibliotece. - Sterowanie Retro na zablokowanym ekranie. - Szczegóły licencji oprogramowania open source - Zaokrąglone krawędzie okna, okładek albumów itp. - Włącz/wyłącz tytuły kart u dołu. - Tryb imersji - Zacznij odtwarzanie po podłączanie zestawu słuchawkowego - Odtwarzanie losowe zostanie wyłączone podczas odtwarzania nowej listy utworów - Włącz sterowanie głośnością na ekranie \"teraz odtwarzane\" - Pokaż okładkę albumu - Wygląd okładki albumu - Przesuwanie okładki albumu - Siatka albumów - Kolorowe skróty aplikacji - Siatka wykonawców - Zmniejsz głośność przy braku skupienia - Automatycznie pobierz obrazki wykonawców - Czarna lista - Odtwarzanie przez Bluetooth - Rozmaż okładkę albumu - Wybierz korektor dźwięku - Klasyczny wygląd powiadomień - Kolor adaptacyjny - Kolorowe powiadomienia - Mniej nasycony kolor - Dodatkowe sterowanie - Informacje o utworze - Odtwarzanie bez przerw - Motyw główny - Pokaż kartę gatunku - Siatka wykonawców strony głównej - Baner strony głównej - Ignoruj okładki z Media Store - Ostatnio dodany interwał listy odtwarzania - Sterowanie pełno ekranowe - Kolorowy pasek nawigacji - Wygląd - Licencje Open Source - Narożne krawędzie - Wygląd tytułów kart - Efekt karuzeli - Kolor dominujący - Aplikacja pełnoekranowa - Tytuły kart - Autoodtwarzanie - Tryb losowy - Sterowanie głośnością - Informacje użytkownika - Kolor podstawowy - Główny kolor motywu, domyślnie niebieski, działa teraz z ciemnymi kolorami - Pro - Efekt karuzeli, kolorowe motywy i wiele więcej... - Profil - Kup - *Zastanów się przed zakupem, nie proś o zwrot. - Kolejka - Oceń aplikację - Uwielbiasz tą aplikację? Daj nam znać w sklepie Google Play co o niej sądzisz i co powinniśmy poprawić. - Ostatnie albumy - Ostatni artyści - Usuń - Usuń zdjęcie banera - Usuń okładkę - Usuń z czarnej listy - Usuń zdjęcie profilowe - Usuń utwór z listy odtwarzania - %1$s z listy odtwarzania?]]> - Usuń te utwory z listy odtwarzania - %1$d z listy odtwarzania?]]> - Zmień nazwę listy odtwarzania - Zgłoś problem - Zgłoś błąd - Zresetuj - Zresetuj obrazek wykonawcy - Przywróć - Przywrócono poprzedni zakup. Zrestartuj aplikacje aby korzystać z wszystkich funkcji. - Przywrócono poprzednie zakupy. - Przywracanie zakupu... - Korektor dźwięku Retro - Retro Music Player - Retro Music Pro - Nie udało się usunąć pliku: %s - - Nie można uzyskać URI SAF - Otwórz szufladę nawigacji - Włącz \'Pokaż kartę SD\' w menu przeciążenia - - %s wymaga dostępu do karty SD - Musisz wybrać katalog główny karty SD - Wybierz kartę SD w panelu nawigacji - Nie otwieraj żadnych podfolderów - Dotknij przycisku \'Wybierz\' u dołu ekranu - Nie udało się zapisać do pliku: %s - Zapisz - - - Zapisz jako plik - Zapisz jako - Zapisz listę odtwarzania do %s. - Zapisywanie zmian - Skanuj media - Zeskanowano %1$d z plików %2$d. - Scrobble - Przeszukaj bibliotekę... - Zaznacz wszystko - Wybierz zdjęcie banera - Zaznaczone - Zgłoś awarię - Ustaw - Ustaw obrazek wykonawcy - Ustaw zdjęcie profilowe - Udostępnij aplikację - Podziel się z opowieściami - Losowo - Prosty - Wyłącznik czasowy wyłączony. - Wyłącznik czasowy ustawiony na %d minut. - Slajd - Mały album - Społeczność - Udostępnij historię - Utwór - Długość utworu - Utwory - Porządek sortowania - Rosnąco - Album - Artysta - Kompozytor - Dane - Data modyfikacji - Rok - Malejąco - Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego - Przeszukaj swoją bibliotekę - Stos - Rozpocznij odtwarzanie - Sugestie - Po prostu pokaż tylko swoje imię na ekranie głównym - Wspieraj rozwój - Przesuń, aby odblokować - Synchronizowany tekst utworu - Systemowy korektor dźwięku - - Telegram - Dołącz do grupy Telegram, aby zgłosić błędy, zasugerować zmiany i inne - Dziękuję! - Pliki dźwiękowy - Ten miesiąc - Ten tydzień - Ten rok - Mały - Tytuł - Ekran główny - Miłego popołudnia - Dzień dobry - Dobry wieczór - Dzień dobry - Dobry wieczór - Jak masz na imię? - Dzisiaj - Najczęściej odtwarzane albumy - Najczęściej odtwarzani artyści - "Ścieżka (2 dla ścieżki 2 lub 3004 dla ścieżki CD3 4)" - Numer utworu - Przetłumacz - Pomóż nam tłumaczyć aplikację na swój język - Strona na Twitterze - Podziel się swoim designem z Retro Music - Niepodpisane - Nie mo\u017cna odtworzy\u0107 utworu. - Następne - Zaktualizuj obrazek - Aktualizowanie... - Nazwa użytkownika - Wersja - Obrót pionowy - Wirtualizacja - Głośność - Przeszukaj sieć - Witaj, - Co chciałbyś udostępnić? - Co nowego - Okno - Zaokrąglone rogi - Ustaw %1$s jako dzwonek. - Wybrano %1$d - Rok - Musisz zaznaczyć przynajmniej jedną kategorię - Będziesz przekierowany do strony ze zgłoszeniami. - Informacje o twoim koncie są używane tylko do autoryzacji. - Ilość - Notatka(Opcjonalna) - Rozpocznij płatność - Wyświetl teraz odtwarzane - Kliknięcie powiadomienia pokaże teraz odtwarzane zamiast ekranu głównego - Mała karta - O %s - Wybierz język - Tłumacze - Osoby, które pomogły przetłumaczyć tę aplikację - Wypróbuj Retro Music Premium - - Utwór - Utwory - Utwory - Utwory - - - Album - Albumy - Albumy - Albumy - - - %d Utwór - %d Utworu - %d Utworu - %d Utworu - - - %d Album - %d Albumu - %d Albumu - %d Albumu - - - %d Artysta - %d Artyści - %d Artyści - %d Artyści - -