From 3660a8bd78f5690620a33aa565b41492391da4d2 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 8 Jun 2022 23:17:15 +0530 Subject: [PATCH 01/37] Revert "[Playback] Code Cleanup" This reverts commit 7efbbc3f110935f8e5ee8bc9f6f35f6bb562d6ae. --- .../monkey/retromusic/service/CastPlayer.kt | 12 ++- .../retromusic/service/CrossFadePlayer.kt | 22 ++++-- .../retromusic/service/LocalPlayback.kt | 18 +++-- .../monkey/retromusic/service/MultiPlayer.kt | 50 +++++++----- .../monkey/retromusic/service/MusicService.kt | 76 ++++++++++--------- .../retromusic/service/PlaybackManager.kt | 10 ++- .../retromusic/service/playback/Playback.kt | 4 +- 7 files changed, 114 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt index e3404f24a..2a67051d6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt @@ -33,15 +33,19 @@ class CastPlayer(castSession: CastSession) : Playback, override var callbacks: Playback.PlaybackCallbacks? = null - override fun setDataSource(song: Song, force: Boolean): Boolean { - return try { + override fun setDataSource( + song: Song, + force: Boolean, + completion: (success: Boolean) -> Unit, + ) { + try { val mediaLoadOptions = MediaLoadOptions.Builder().setPlayPosition(0).setAutoplay(true).build() remoteMediaClient?.load(song.toMediaInfo()!!, mediaLoadOptions) - true + completion(true) } catch (e: Exception) { e.printStackTrace() - false + completion(false) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt index af9661feb..da27e6b36 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt @@ -120,17 +120,25 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) { override val isPlaying: Boolean get() = mIsInitialized && getCurrentPlayer()?.isPlaying == true - override fun setDataSource(song: Song, force: Boolean): Boolean { + override fun setDataSource( + song: Song, + force: Boolean, + completion: (success: Boolean) -> Unit, + ) { if (force) hasDataSource = false mIsInitialized = false /* We've already set DataSource if initialized is true in setNextDataSource */ - return if (!hasDataSource) { - mIsInitialized = setDataSourceImpl(getCurrentPlayer()!!, song.uri.toString()) + if (!hasDataSource) { + getCurrentPlayer()?.let { + setDataSourceImpl(it, song.uri.toString()) { success -> + mIsInitialized = success + completion(success) + } + } hasDataSource = true - mIsInitialized } else { + completion(true) mIsInitialized = true - true } } @@ -285,8 +293,8 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) { val nextSong = MusicPlayerRemote.nextSong // Switch to other player (Crossfade) only if next song exists if (nextSong != null) { - if (setDataSourceImpl(player, nextSong.uri.toString())) { - switchPlayer() + setDataSourceImpl(player, nextSong.uri.toString()) { success -> + if (success) switchPlayer() } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt b/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt index ee039f42c..fafbae0ec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt @@ -109,10 +109,13 @@ abstract class LocalPlayback(val context: Context) : Playback, MediaPlayer.OnErr * @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 */ - fun setDataSourceImpl(player: MediaPlayer, path: String): Boolean { + fun setDataSourceImpl( + player: MediaPlayer, + path: String, + completion: (success: Boolean) -> Unit, + ) { + player.reset() try { - player.reset() - player.setOnPreparedListener(null) if (path.startsWith("content://")) { player.setDataSource(context, path.toUri()) } else { @@ -123,14 +126,17 @@ abstract class LocalPlayback(val context: Context) : Playback, MediaPlayer.OnErr .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) - player.prepare() + player.setOnPreparedListener { + player.setOnPreparedListener(null) + completion(true) + } + player.prepareAsync() } catch (e: Exception) { + completion(false) e.printStackTrace() - return false } player.setOnCompletionListener(this) player.setOnErrorListener(this) - return true } private fun unregisterBecomingNoisyReceiver() { diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.kt index d0e766b3f..8421b60f1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MultiPlayer.kt @@ -46,13 +46,19 @@ class MultiPlayer(context: Context) : LocalPlayback(context) { * @param song The song object you want to play * @return True if the `player` has been prepared and is ready to play, false otherwise */ - override fun setDataSource(song: Song, force: Boolean): Boolean { + override fun setDataSource( + song: Song, + force: Boolean, + completion: (success: Boolean) -> Unit, + ) { isInitialized = false - isInitialized = setDataSourceImpl(mCurrentMediaPlayer, song.uri.toString()) - if (isInitialized) { - setNextDataSource(null) + setDataSourceImpl(mCurrentMediaPlayer, song.uri.toString()) { success -> + isInitialized = success + if (isInitialized) { + setNextDataSource(null) + } + completion(isInitialized) } - return isInitialized } /** @@ -80,26 +86,28 @@ class MultiPlayer(context: Context) : LocalPlayback(context) { mNextMediaPlayer = MediaPlayer() mNextMediaPlayer?.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) mNextMediaPlayer?.audioSessionId = audioSessionId - if (setDataSourceImpl(mNextMediaPlayer!!, path)) { - try { - mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer) - } catch (e: IllegalArgumentException) { - Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e) + setDataSourceImpl(mNextMediaPlayer!!, path) { success -> + if (success) { + try { + mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer) + } catch (e: IllegalArgumentException) { + Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e) + if (mNextMediaPlayer != null) { + mNextMediaPlayer?.release() + mNextMediaPlayer = null + } + } catch (e: IllegalStateException) { + Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e) + if (mNextMediaPlayer != null) { + mNextMediaPlayer?.release() + mNextMediaPlayer = null + } + } + } else { if (mNextMediaPlayer != null) { mNextMediaPlayer?.release() mNextMediaPlayer = null } - } catch (e: IllegalStateException) { - Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e) - if (mNextMediaPlayer != null) { - mNextMediaPlayer?.release() - mNextMediaPlayer = null - } - } - } else { - if (mNextMediaPlayer != null) { - mNextMediaPlayer?.release() - mNextMediaPlayer = null } } } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index cee0a47ed..512cd3839 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -117,11 +117,6 @@ class MusicService : MediaBrowserServiceCompat(), private var trackEndedByCrossfade = false private val serviceScope = CoroutineScope(Job() + Main) - // Every chromecast method needs to run on main thread or you are greeted with IllegalStateException - // So it will use Main dispatcher - // And by using Default dispatcher for local playback we are reducing the burden from main thread - private val playerDispatcher get() = if (playbackManager.isLocalPlayback) Default else Main - @JvmField var position = -1 private val appWidgetBig = AppWidgetBig.instance @@ -441,8 +436,11 @@ class MusicService : MediaBrowserServiceCompat(), } private fun setPosition(position: Int) { - openTrackAndPrepareNextAt(position) - notifyChange(PLAY_STATE_CHANGED) + openTrackAndPrepareNextAt(position) { success -> + if (success) { + notifyChange(PLAY_STATE_CHANGED) + } + } } private fun getPreviousPosition(force: Boolean): Int { @@ -757,15 +755,16 @@ class MusicService : MediaBrowserServiceCompat(), } @Synchronized - fun openTrackAndPrepareNextAt(position: Int): Boolean { + fun openTrackAndPrepareNextAt(position: Int, completion: (success: Boolean) -> Unit) { this.position = position - val prepared = openCurrent() - if (prepared) { - prepareNextImpl() + openCurrent { success -> + completion(success) + notifyChange(META_CHANGED) + notHandledMetaChangedForCurrentTrack = false + if (success) { + prepareNextImpl() + } } - notifyChange(META_CHANGED) - notHandledMetaChangedForCurrentTrack = false - return prepared } fun pause(force: Boolean = false) { @@ -794,11 +793,16 @@ class MusicService : MediaBrowserServiceCompat(), } fun playSongAt(position: Int) { - serviceScope.launch(playerDispatcher) { - if (openTrackAndPrepareNextAt(position)) { - play() - } else { - showToast(R.string.unplayable_file) + // Every chromecast method needs to run on main thread or you are greeted with IllegalStateException + // So it will use Main dispatcher + // And by using Default dispatcher for local playback we are reduce the burden of main thread + serviceScope.launch(if(playbackManager.isLocalPlayback) Default else Main) { + openTrackAndPrepareNextAt(position) { success -> + if (success) { + play() + } else { + showToast(resources.getString(R.string.unplayable_file)) + } } } } @@ -913,14 +917,15 @@ class MusicService : MediaBrowserServiceCompat(), originalPlayingQueue = ArrayList(restoredOriginalQueue) playingQueue = ArrayList(restoredQueue) position = restoredPosition - withContext(playerDispatcher) { - openCurrent() - prepareNext() - if (restoredPositionInTrack > 0) { - seek(restoredPositionInTrack) + withContext(Main) { + openCurrent { + prepareNext() + if (restoredPositionInTrack > 0) { + seek(restoredPositionInTrack) + } + notHandledMetaChangedForCurrentTrack = true + sendChangeInternal(META_CHANGED) } - notHandledMetaChangedForCurrentTrack = true - sendChangeInternal(META_CHANGED) if (receivedHeadsetConnected) { play() receivedHeadsetConnected = false @@ -1164,18 +1169,15 @@ class MusicService : MediaBrowserServiceCompat(), } @Synchronized - private fun openCurrent(): Boolean { + private fun openCurrent(completion: (success: Boolean) -> Unit) { val force = if (!trackEndedByCrossfade) { true } else { trackEndedByCrossfade = false false } - return try { - playbackManager.setDataSource(currentSong, force) - } catch (e: Exception) { - e.printStackTrace() - false + playbackManager.setDataSource(currentSong, force) { success -> + completion(success) } } @@ -1189,10 +1191,12 @@ class MusicService : MediaBrowserServiceCompat(), private fun restorePlaybackState(wasPlaying: Boolean, progress: Int) { playbackManager.setCallbacks(this) - if (openTrackAndPrepareNextAt(position)) { - seek(progress) - if (wasPlaying) { - play() + openTrackAndPrepareNextAt(position) { success -> + if (success) { + seek(progress) + if (wasPlaying) { + play() + } } } playbackManager.setCrossFadeDuration(crossFadeDuration) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt index f64b7d600..9186bd534 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt @@ -16,7 +16,7 @@ class PlaybackManager(val context: Context) { var playback: Playback? = null private var playbackLocation = PlaybackLocation.LOCAL - val isLocalPlayback get() = playbackLocation == PlaybackLocation.LOCAL + val isLocalPlayback get() = playbackLocation== PlaybackLocation.LOCAL val audioSessionId: Int get() = if (playback != null) { @@ -86,8 +86,12 @@ class PlaybackManager(val context: Context) { fun seek(millis: Int): Int = playback!!.seek(millis) - fun setDataSource(song: Song, force: Boolean): Boolean { - return playback?.setDataSource(song, force) == true + fun setDataSource( + song: Song, + force: Boolean, + completion: (success: Boolean) -> Unit, + ) { + playback?.setDataSource(song, force, completion) } fun setNextDataSource(trackUri: String) { diff --git a/app/src/main/java/code/name/monkey/retromusic/service/playback/Playback.kt b/app/src/main/java/code/name/monkey/retromusic/service/playback/Playback.kt index f3521ab96..3795e7634 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/playback/Playback.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/playback/Playback.kt @@ -25,7 +25,9 @@ interface Playback { val audioSessionId: Int - fun setDataSource(song: Song, force: Boolean):Boolean + fun setDataSource( + song: Song, force: Boolean, completion: (success: Boolean) -> Unit, + ) fun setNextDataSource(path: String?) From ceec034a4ffdd06426c2ba4fea14c2c7eac5efa0 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 8 Jun 2022 23:20:43 +0530 Subject: [PATCH 02/37] Revert "Start audio fade animator on Main thread" This reverts commit 46f713e6884be08e00541e4fa0646cb2f6cb5a12. --- .../java/code/name/monkey/retromusic/service/AudioFader.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt index d79df4e43..009b0c40d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt @@ -3,8 +3,6 @@ package code.name.monkey.retromusic.service import android.animation.Animator import android.animation.ValueAnimator import android.media.MediaPlayer -import android.os.Handler -import android.os.Looper import androidx.core.animation.doOnEnd import code.name.monkey.retromusic.service.playback.Playback import code.name.monkey.retromusic.util.PreferenceUtil @@ -59,9 +57,7 @@ class AudioFader { animator.doOnEnd { callback.run() } - Handler(Looper.getMainLooper()).post { - animator.start() - } + animator.start() } } } \ No newline at end of file From 75a4648e13a6053d20bdf899f358f1e9a1195384 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Thu, 9 Jun 2022 01:06:56 +0530 Subject: [PATCH 03/37] Fixed CrossFade duration not changing instantly --- .../retromusic/fragments/other/LyricsFragment.kt | 15 +-------------- .../monkey/retromusic/service/MusicService.kt | 2 ++ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt index 8dbaab6a7..e2eaf6c9b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt @@ -77,14 +77,6 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { baseUrl += query return baseUrl } - private val syairSearchLrcUrl: String - get() { - var baseUrl = "https://www.syair.info/search?" - var query = song.title + "+" + song.artistName - query = "q=" + query.replace(" ", "+") - baseUrl += query - return baseUrl - } private lateinit var normalLyricsLauncher: ActivityResultLauncher private lateinit var newSyncedLyricsLauncher: ActivityResultLauncher @@ -201,12 +193,7 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { override fun onMenuItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.action_search) { - openUrl(when (binding.lyricsPager.currentItem) { - 0 -> syairSearchLrcUrl - 1 -> googleSearchLrcUrl - else -> googleSearchLrcUrl - } - ) + openUrl(googleSearchLrcUrl) } return false } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index 512cd3839..16a550136 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -632,6 +632,8 @@ class MusicService : MediaBrowserServiceCompat(), if (playbackManager.maybeSwitchToCrossFade(crossFadeDuration)) { restorePlaybackState(wasPlaying, progress) + } else { + playbackManager.setCrossFadeDuration(crossFadeDuration) } } ALBUM_ART_ON_LOCK_SCREEN, BLURRED_ALBUM_ART -> updateMediaSessionMetaData(::updateMediaSessionPlaybackState) From bca9fb0b5a72946e4c1ef76e8d70d11b6ea58a12 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Thu, 9 Jun 2022 14:36:31 +0530 Subject: [PATCH 04/37] Fixed some landscape layouts --- app/build.gradle | 5 +- .../layout-land/fragment_adaptive_player.xml | 72 +++++++++++++++++++ .../layout-land/fragment_circle_player.xml | 14 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/layout-land/fragment_adaptive_player.xml diff --git a/app/build.gradle b/app/build.gradle index e203f7ea1..242402243 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10590 + versionCode 10591 versionName '6.0.0-beta' buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"") @@ -125,7 +125,7 @@ dependencies { def retrofit_version = '2.9.0' implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.7' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.8' def material_dialog_version = "3.3.0" implementation "com.afollestad.material-dialogs:core:$material_dialog_version" @@ -167,5 +167,4 @@ dependencies { implementation 'me.zhanghai.android.fastscroll:library:1.1.8' implementation 'cat.ereza:customactivityoncrash:2.4.0' implementation 'me.tankery.lib:circularSeekBar:1.4.0' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' } \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_adaptive_player.xml b/app/src/main/res/layout-land/fragment_adaptive_player.xml new file mode 100644 index 000000000..6a193abcb --- /dev/null +++ b/app/src/main/res/layout-land/fragment_adaptive_player.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_circle_player.xml b/app/src/main/res/layout-land/fragment_circle_player.xml index 71f388878..d1e87ac7f 100644 --- a/app/src/main/res/layout-land/fragment_circle_player.xml +++ b/app/src/main/res/layout-land/fragment_circle_player.xml @@ -112,7 +112,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" + android:background="?attr/roundSelector" android:padding="16dp" + android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@+id/playPauseButton" app:layout_constraintStart_toEndOf="@+id/playPauseButton" app:layout_constraintTop_toTopOf="@+id/playPauseButton" @@ -124,7 +126,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" + android:background="?attr/roundSelector" android:padding="16dp" + android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@+id/playPauseButton" app:layout_constraintEnd_toStartOf="@+id/playPauseButton" app:layout_constraintTop_toTopOf="@+id/playPauseButton" @@ -195,7 +199,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" - android:gravity="center_vertical|right|end" + android:gravity="center_vertical|end" android:singleLine="true" android:textColor="?android:attr/textColorSecondary" android:textSize="12sp" @@ -209,9 +213,11 @@ android:id="@+id/songCurrentProgress" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:gravity="center_vertical|left|end" - android:paddingLeft="8dp" + android:layout_alignParentStart="true" + android:layout_marginStart="16dp" + android:gravity="center_vertical|start" + android:paddingStart="8dp" + android:paddingEnd="0dp" android:singleLine="true" android:textColor="?android:attr/textColorSecondary" android:textSize="12sp" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6308cc5cc..20c531b95 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip From 9966ddad9dcd73711f7ebae5a8b7efb584e85a4a Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Fri, 10 Jun 2022 19:55:05 +0530 Subject: [PATCH 05/37] Update dependencies --- app/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 242402243..fb471cfc6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,7 +105,7 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" - def room_version = '2.4.2' + def room_version = '2.5.0-alpha02' implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" diff --git a/build.gradle b/build.gradle index d1d3d8338..431658a9a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.6.21' + kotlin_version = '1.7.0' navigation_version = '2.5.0-rc01' mdc_version = '1.7.0-alpha02' preference_version = '1.2.0' From ed6ca486d6c6a053412ab56dd484b6152ea1b2ed Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Fri, 10 Jun 2022 22:58:26 +0530 Subject: [PATCH 06/37] Fixed ChromeCast crashes --- .../monkey/retromusic/service/CastPlayer.kt | 3 +- .../monkey/retromusic/service/MusicService.kt | 43 +++++++------------ .../retromusic/service/PlaybackManager.kt | 13 +++--- .../retromusic/service/QueueSaveHandler.kt | 35 --------------- 4 files changed, 21 insertions(+), 73 deletions(-) delete mode 100644 app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt index 2a67051d6..514612e33 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt @@ -10,8 +10,7 @@ import com.google.android.gms.cast.MediaStatus import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.media.RemoteMediaClient -class CastPlayer(castSession: CastSession) : Playback, - RemoteMediaClient.Callback() { +class CastPlayer(castSession: CastSession) : Playback, RemoteMediaClient.Callback() { override val isInitialized: Boolean = true diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index 16a550136..ee1db4f32 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -203,8 +203,7 @@ class MusicService : MediaBrowserServiceCompat(), } } } - private var queueSaveHandler: QueueSaveHandler? = null - private var queueSaveHandlerThread: HandlerThread? = null + private var queuesRestored = false var repeatMode = 0 @@ -277,12 +276,6 @@ class MusicService : MediaBrowserServiceCompat(), playbackManager.setCallbacks(this) setupMediaSession() - // queue saving needs to run on a separate thread so that it doesn't block the playback handler - // events - queueSaveHandlerThread = - HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND) - queueSaveHandlerThread?.start() - queueSaveHandler = QueueSaveHandler(this, queueSaveHandlerThread!!.looper) uiThreadHandler = Handler(Looper.getMainLooper()) registerReceiver(widgetIntentReceiver, IntentFilter(APP_WIDGET_UPDATE)) registerReceiver(updateFavoriteReceiver, IntentFilter(FAVORITE_STATE_CHANGED)) @@ -291,7 +284,7 @@ class MusicService : MediaBrowserServiceCompat(), notificationManager = getSystemService() initNotification() mediaStoreObserver = MediaStoreObserver(this, playerHandler!!) - throttledSeekHandler = ThrottledSeekHandler(this, playerHandler!!) + throttledSeekHandler = ThrottledSeekHandler(this, Handler(mainLooper)) contentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, mediaStoreObserver) @@ -798,7 +791,7 @@ class MusicService : MediaBrowserServiceCompat(), // Every chromecast method needs to run on main thread or you are greeted with IllegalStateException // So it will use Main dispatcher // And by using Default dispatcher for local playback we are reduce the burden of main thread - serviceScope.launch(if(playbackManager.isLocalPlayback) Default else Main) { + serviceScope.launch(if (playbackManager.isLocalPlayback) Default else Main) { openTrackAndPrepareNextAt(position) { success -> if (success) { play() @@ -953,17 +946,6 @@ class MusicService : MediaBrowserServiceCompat(), } } - fun saveQueuesImpl() { - MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue) - } - - fun saveState() { - saveQueues() - savePosition() - savePositionInTrack() - storage.saveSong(currentSong) - } - @Synchronized fun seek(millis: Int): Int { return try { @@ -1105,8 +1087,10 @@ class MusicService : MediaBrowserServiceCompat(), // if we are loading it or it won't be updated in the notification updateMediaSessionMetaData(::updateMediaSessionPlaybackState) serviceScope.launch(IO) { - savePosition() - savePositionInTrack() + withContext(Main) { + savePosition() + savePositionInTrack() + } val currentSong = currentSong HistoryStore.getInstance(this@MusicService).addSongId(currentSong.id) if (songPlayCountHelper.shouldBumpPlayCount()) { @@ -1114,13 +1098,14 @@ class MusicService : MediaBrowserServiceCompat(), .bumpPlayCount(songPlayCountHelper.song.id) } songPlayCountHelper.notifySongChanged(currentSong) + storage.saveSong(currentSong) } } QUEUE_CHANGED -> { mediaSession?.setQueueTitle(getString(R.string.now_playing_queue)) mediaSession?.setQueue(playingQueue.toMediaSessionQueue()) updateMediaSessionMetaData(::updateMediaSessionPlaybackState) // because playing queue size might have changed - saveState() + saveQueues() if (playingQueue.size > 0) { prepareNext() } else { @@ -1198,6 +1183,8 @@ class MusicService : MediaBrowserServiceCompat(), seek(progress) if (wasPlaying) { play() + } else { + pause() } } } @@ -1247,8 +1234,6 @@ class MusicService : MediaBrowserServiceCompat(), private fun releaseResources() { playerHandler?.removeCallbacksAndMessages(null) musicPlayerHandlerThread?.quitSafely() - queueSaveHandler?.removeCallbacksAndMessages(null) - queueSaveHandlerThread?.quitSafely() playbackManager.release() mediaSession?.release() } @@ -1275,8 +1260,10 @@ class MusicService : MediaBrowserServiceCompat(), } private fun saveQueues() { - queueSaveHandler?.removeMessages(SAVE_QUEUES) - queueSaveHandler?.sendEmptyMessage(SAVE_QUEUES) + serviceScope.launch(IO) { + MusicPlaybackQueueStore.getInstance(this@MusicService) + .saveQueues(playingQueue, originalPlayingQueue) + } } private fun sendChangeInternal(what: String) { diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt index 9186bd534..983968044 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt @@ -16,7 +16,7 @@ class PlaybackManager(val context: Context) { var playback: Playback? = null private var playbackLocation = PlaybackLocation.LOCAL - val isLocalPlayback get() = playbackLocation== PlaybackLocation.LOCAL + val isLocalPlayback get() = playbackLocation == PlaybackLocation.LOCAL val audioSessionId: Int get() = if (playback != null) { @@ -168,14 +168,11 @@ class PlaybackManager(val context: Context) { playback: Playback, onChange: (wasPlaying: Boolean, progress: Int) -> Unit, ) { - val oldPlayback = playback - val wasPlaying: Boolean = oldPlayback.isPlaying - val progress: Int = oldPlayback.position() - + val oldPlayback = this.playback + val wasPlaying: Boolean = oldPlayback?.isPlaying == true + val progress: Int = oldPlayback?.position() ?: 0 this.playback = playback - - oldPlayback.stop() - + oldPlayback?.stop() onChange(wasPlaying, progress) } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt b/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt deleted file mode 100644 index 9fd1b91ad..000000000 --- a/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2019 Hemanth Savarala. - * - * 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.service - -import android.os.Handler -import android.os.Looper -import android.os.Message -import code.name.monkey.retromusic.service.MusicService.Companion.SAVE_QUEUES -import java.lang.ref.WeakReference - -internal class QueueSaveHandler( - musicService: MusicService, - looper: Looper -) : Handler(looper) { - private val service: WeakReference = WeakReference(musicService) - - override fun handleMessage(msg: Message) { - val service: MusicService? = service.get() - if (msg.what == SAVE_QUEUES) { - service?.saveQueuesImpl() - } - } -} From aedabb8b74c7681d642a34dd5b7970e4c92b75fd Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sat, 11 Jun 2022 00:44:59 +0530 Subject: [PATCH 07/37] Update Changelog --- app/build.gradle | 4 ++-- app/src/main/assets/retro-changelog.html | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fb471cfc6..f9487b0e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10591 - versionName '6.0.0-beta' + versionCode 10592 + versionName '6.0.1-beta' buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"") } diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html index 9c9ea8418..45bbf979d 100644 --- a/app/src/main/assets/retro-changelog.html +++ b/app/src/main/assets/retro-changelog.html @@ -62,6 +62,14 @@ +
+
June 13, 2022
+

v6.0.1Beta

+

Fixed

+
    +
  • Fixed ChromeCast crash
  • +
+
June 7, 2022

v6.0.0Beta

From 3ab5e1ddc3f603784f4d95f8e7c1ff830d9edfe6 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sat, 11 Jun 2022 10:43:38 +0530 Subject: [PATCH 08/37] Added Scrollbar to Playlist details fragments --- .../retromusic/fragments/playlists/PlaylistDetailsFragment.kt | 2 ++ 1 file changed, 2 insertions(+) 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 5dc7aad09..1a71944c1 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,6 +24,7 @@ import code.name.monkey.retromusic.interfaces.ICabCallback import code.name.monkey.retromusic.interfaces.ICabHolder import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.RetroColorUtil +import code.name.monkey.retromusic.util.ThemedFastScroller import com.afollestad.materialcab.attached.AttachedCab import com.afollestad.materialcab.attached.destroy import com.afollestad.materialcab.attached.isActive @@ -109,6 +110,7 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli binding.recyclerView.apply { layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = wrappedAdapter + ThemedFastScroller.create(this) } playlistSongAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { From cf4c773a67239139c3f4bfc6a45533f0fde99874 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sat, 11 Jun 2022 17:19:24 +0530 Subject: [PATCH 09/37] [MiniPlayer] Code Cleanup --- .../retromusic/fragments/other/MiniPlayerFragment.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt index fe3180e64..552773b91 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt @@ -14,7 +14,6 @@ */ package code.name.monkey.retromusic.fragments.other -import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.os.Bundle @@ -23,7 +22,6 @@ import android.text.style.ForegroundColorSpan import android.view.GestureDetector import android.view.MotionEvent import android.view.View -import android.view.animation.DecelerateInterpolator import androidx.core.text.toSpannable import androidx.core.view.isVisible import code.name.monkey.retromusic.R @@ -139,10 +137,7 @@ open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_p override fun onUpdateProgressViews(progress: Int, total: Int) { binding.progressBar.max = total - val animator = ObjectAnimator.ofInt(binding.progressBar, "progress", progress) - animator.duration = 1000 - animator.interpolator = DecelerateInterpolator() - animator.start() + binding.progressBar.progress = progress } override fun onResume() { From 3aea91eb1e4917e71c4b47985b569c35ab0adf87 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sat, 11 Jun 2022 17:28:17 +0530 Subject: [PATCH 10/37] Safely show toasts in MusicService --- .../name/monkey/retromusic/service/MusicService.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index ee1db4f32..f83b6f0d2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -796,7 +796,9 @@ class MusicService : MediaBrowserServiceCompat(), if (success) { play() } else { - showToast(resources.getString(R.string.unplayable_file)) + runOnUiThread { + showToast(R.string.unplayable_file) + } } } } @@ -1205,10 +1207,14 @@ class MusicService : MediaBrowserServiceCompat(), openQueue(playlistSongs, 0, true) } } else { - showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG) + runOnUiThread { + showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG) + } } } else { - showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG) + runOnUiThread { + showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG) + } } } @@ -1355,7 +1361,6 @@ class MusicService : MediaBrowserServiceCompat(), const val REPEAT_MODE_NONE = 0 const val REPEAT_MODE_ALL = 1 const val REPEAT_MODE_THIS = 2 - const val SAVE_QUEUES = 0 private const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PLAY_PAUSE From 192b5c2ac7ac85e4922bb928b83bc87cf66bd007 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sat, 11 Jun 2022 19:02:00 +0530 Subject: [PATCH 11/37] Enabled WRITE_EXTERNAL_STORAGE for A10 to avoid crashes --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 +- .../activities/base/AbsMusicServiceActivity.kt | 2 +- .../name/monkey/retromusic/service/MusicService.kt | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f9487b0e0..f92862ea9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,7 +51,7 @@ android { } } lint { - disable 'MissingTranslation', 'ImpliedQuantity' + warning 'MissingTranslation', 'ImpliedQuantity', 'Instantiatable' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dabe48036..697a4cb6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ + android:maxSdkVersion="29" /> 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 59c6320d8..07937c7b5 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 @@ -191,7 +191,7 @@ abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventLi override fun getPermissionsToRequest(): Array { return mutableListOf(Manifest.permission.READ_EXTERNAL_STORAGE).apply { - if (!VersionUtils.hasQ()) { + if (!VersionUtils.hasR()) { add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } }.toTypedArray() diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index f83b6f0d2..d4ac50db0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -754,11 +754,11 @@ class MusicService : MediaBrowserServiceCompat(), this.position = position openCurrent { success -> completion(success) - notifyChange(META_CHANGED) - notHandledMetaChangedForCurrentTrack = false if (success) { prepareNextImpl() } + notifyChange(META_CHANGED) + notHandledMetaChangedForCurrentTrack = false } } @@ -1088,11 +1088,9 @@ class MusicService : MediaBrowserServiceCompat(), // We must call updateMediaSessionPlaybackState after the load of album art is completed // if we are loading it or it won't be updated in the notification updateMediaSessionMetaData(::updateMediaSessionPlaybackState) + savePosition() + savePositionInTrack() serviceScope.launch(IO) { - withContext(Main) { - savePosition() - savePositionInTrack() - } val currentSong = currentSong HistoryStore.getInstance(this@MusicService).addSongId(currentSong.id) if (songPlayCountHelper.shouldBumpPlayCount()) { From e4a6906231b5011974600c3d5b7480374d552039 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 20:15:43 +0530 Subject: [PATCH 12/37] Revert "Use MediaButtonReceiver from androidx.media to handle headset button actions" This reverts commit ad51d0967215effff2d00f59ef75d75318bc2ef2. --- app/src/main/AndroidManifest.xml | 7 +- .../service/MediaButtonIntentReceiver.kt | 201 ++++++++++++++++++ .../monkey/retromusic/service/MusicService.kt | 18 +- 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 697a4cb6f..efbc081f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,12 +34,12 @@ android:configChanges="locale|layoutDirection" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:requestLegacyExternalStorage="true" android:restoreAnyVersion="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.RetroMusic.FollowSystem" android:usesCleartextTraffic="true" - android:requestLegacyExternalStorage="true" tools:targetApi="m"> @@ -305,9 +305,6 @@ - - - { + val clickCount = msg.arg1 + + if (DEBUG) Log.v(TAG, "Handling headset click, count = $clickCount") + val command = when (clickCount) { + 1 -> ACTION_TOGGLE_PAUSE + 2 -> ACTION_SKIP + 3 -> ACTION_REWIND + else -> null + } + + if (command != null) { + val context = msg.obj as Context + startService(context, command) + } + } + } + releaseWakeLockIfHandlerIdle() + } + } + + fun handleIntent(context: Context, intent: Intent): Boolean { + println("Intent Action: ${intent.action}") + val intentAction = intent.action + if (Intent.ACTION_MEDIA_BUTTON == intentAction) { + val event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + ?: return false + + val keycode = event.keyCode + val action = event.action + val eventTime = if (event.eventTime != 0L) + event.eventTime + else + System.currentTimeMillis() + + var command: String? = null + when (keycode) { + KeyEvent.KEYCODE_MEDIA_STOP -> command = ACTION_STOP + KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> command = + ACTION_TOGGLE_PAUSE + KeyEvent.KEYCODE_MEDIA_NEXT -> command = ACTION_SKIP + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> command = ACTION_REWIND + KeyEvent.KEYCODE_MEDIA_PAUSE -> command = ACTION_PAUSE + KeyEvent.KEYCODE_MEDIA_PLAY -> command = ACTION_PLAY + } + if (command != null) { + if (action == KeyEvent.ACTION_DOWN) { + if (event.repeatCount == 0) { + // Only consider the first event in a sequence, not the repeat events, + // so that we don't trigger in cases where the first event went to + // a different app (e.g. when the user ends a phone call by + // long pressing the headset button) + + // The service may or may not be running, but we need to send it + // a command. + if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { + if (eventTime - mLastClickTime >= DOUBLE_CLICK) { + mClickCounter = 0 + } + + mClickCounter++ + if (DEBUG) Log.v(TAG, "Got headset click, count = $mClickCounter") + mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT) + + val msg = mHandler.obtainMessage( + MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context + ) + + val delay = (if (mClickCounter < 3) DOUBLE_CLICK else 0).toLong() + if (mClickCounter >= 3) { + mClickCounter = 0 + } + mLastClickTime = eventTime + acquireWakeLockAndSendMessage(context, msg, delay) + } else { + startService(context, command) + } + return true + } + } + } + } + return false + } + + private fun startService(context: Context, command: String?) { + val intent = Intent(context, MusicService::class.java) + intent.action = command + try { + // IMPORTANT NOTE: (kind of a hack) + // on Android O and above the following crashes when the app is not running + // there is no good way to check whether the app is running so we catch the exception + // we do not always want to use startForegroundService() because then one gets an ANR + // if no notification is displayed via startForeground() + // according to Play analytics this happens a lot, I suppose for example if command = PAUSE + context.startService(intent) + } catch (ignored: IllegalStateException) { + ContextCompat.startForegroundService(context, intent) + } + } + + private fun acquireWakeLockAndSendMessage(context: Context, msg: Message, delay: Long) { + if (wakeLock == null) { + val appContext = context.applicationContext + val pm = appContext.getSystemService() + wakeLock = pm?.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "RetroMusicApp:Wakelock headset button" + ) + wakeLock!!.setReferenceCounted(false) + } + if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what) + // Make sure we don't indefinitely hold the wake lock under any circumstances + wakeLock!!.acquire(10000) + + mHandler.sendMessageDelayed(msg, delay) + } + + private fun releaseWakeLockIfHandlerIdle() { + if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) { + if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock") + return + } + + if (wakeLock != null) { + if (DEBUG) Log.v(TAG, "Releasing wake lock") + wakeLock!!.release() + wakeLock = null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index d4ac50db0..c45d9b83e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -15,6 +15,7 @@ package code.name.monkey.retromusic.service import android.annotation.SuppressLint import android.app.NotificationManager +import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.EXTRA_DEVICE @@ -39,7 +40,6 @@ import android.widget.Toast import androidx.core.content.edit import androidx.core.content.getSystemService import androidx.media.MediaBrowserServiceCompat -import androidx.media.session.MediaButtonReceiver.handleIntent import androidx.preference.PreferenceManager import code.name.monkey.appthemehelper.util.VersionUtils import code.name.monkey.retromusic.* @@ -94,6 +94,7 @@ import kotlinx.coroutines.Dispatchers.Main import org.koin.java.KoinJavaComponent.get import java.util.* + /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal. Modified by Prathamesh More */ @@ -649,7 +650,6 @@ class MusicService : MediaBrowserServiceCompat(), override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null && intent.action != null) { - handleIntent(mediaSession, intent) serviceScope.launch { restoreQueuesAndPositionIfNecessary() when (intent.action) { @@ -1305,13 +1305,25 @@ class MusicService : MediaBrowserServiceCompat(), } private fun setupMediaSession() { + val mediaButtonReceiverComponentName = ComponentName(applicationContext, + MediaButtonIntentReceiver::class.java) + + val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) + mediaButtonIntent.component = mediaButtonReceiverComponentName + val mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast( + applicationContext, 0, mediaButtonIntent, + if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE else 0 + ) mediaSession = MediaSessionCompat( this, - "RetroMusicPlayer" + BuildConfig.APPLICATION_ID, + mediaButtonReceiverComponentName, + mediaButtonReceiverPendingIntent ) val mediaSessionCallback = MediaSessionCallback(this) mediaSession?.setCallback(mediaSessionCallback) mediaSession?.isActive = true + mediaSession?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent) } inner class MusicBinder : Binder() { From 8f566630591dd65a11549253f522e6655dca0d43 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 20:16:52 +0530 Subject: [PATCH 13/37] Code Cleanup --- .../player/PlayerAlbumCoverFragment.kt | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt index d0db68330..3770da872 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt @@ -52,7 +52,6 @@ import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.util.logD import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_player_album_cover), ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback, @@ -86,22 +85,18 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe } private fun updateLyrics() { - binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found)) val song = MusicPlayerRemote.currentSong lifecycleScope.launch(Dispatchers.IO) { val lrcFile = LyricUtil.getSyncedLyricsFile(song) if (lrcFile != null) { - withContext(Dispatchers.Main) { - binding.lyricsView.loadLrc(lrcFile) - } + binding.lyricsView.loadLrc(lrcFile) } else { val embeddedLyrics = LyricUtil.getEmbeddedSyncedLyrics(song.data) - withContext(Dispatchers.Main) { - if (embeddedLyrics != null) { - binding.lyricsView.loadLrc(embeddedLyrics) - } else { - binding.lyricsView.reset() - } + if (embeddedLyrics != null) { + binding.lyricsView.loadLrc(embeddedLyrics) + } else { + binding.lyricsView.reset() + binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found)) } } } From 6f12a7b24ad37fcbb77734b0ea2619d264bc7335 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 20:56:58 +0530 Subject: [PATCH 14/37] Fix Slider crashes --- .../retromusic/activities/DriveModeActivity.kt | 13 ++++--------- .../fragments/base/AbsPlayerControlsFragment.kt | 5 ++--- .../player/circle/CirclePlayerFragment.kt | 15 ++++----------- .../player/gradient/GradientPlayerFragment.kt | 15 +++------------ .../name/monkey/retromusic/service/AudioFader.kt | 2 +- .../monkey/retromusic/service/CrossFadePlayer.kt | 2 +- 6 files changed, 15 insertions(+), 37 deletions(-) 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 1fb04f469..9c6178aaf 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 @@ -14,12 +14,10 @@ */ package code.name.monkey.retromusic.activities -import android.animation.ObjectAnimator import android.content.Intent import android.graphics.Color import android.graphics.PorterDuff import android.os.Bundle -import android.view.animation.LinearInterpolator import androidx.lifecycle.lifecycleScope import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity @@ -27,7 +25,6 @@ import code.name.monkey.retromusic.databinding.ActivityDriveModeBinding import code.name.monkey.retromusic.db.toSongEntity import code.name.monkey.retromusic.extensions.accentColor import code.name.monkey.retromusic.extensions.drawAboveSystemBars -import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment import code.name.monkey.retromusic.glide.BlurTransformation import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension @@ -246,12 +243,10 @@ class DriveModeActivity : AbsMusicServiceActivity(), Callback { } override fun onUpdateProgressViews(progress: Int, total: Int) { - binding.progressSlider.valueTo = total.toFloat() - - val animator = ObjectAnimator.ofFloat(binding.progressSlider, "value", progress.toFloat()) - animator.duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME - animator.interpolator = LinearInterpolator() - animator.start() + binding.progressSlider.run { + valueTo = total.toFloat() + value = progress.toFloat().coerceIn(valueFrom, valueTo) + } binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong()) binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong()) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt index 0a12de582..78659cd50 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt @@ -82,9 +82,8 @@ abstract class AbsPlayerControlsFragment(@LayoutRes layout: Int) : AbsMusicServi if (seekBar == null) { progressSlider?.valueTo = total.toFloat() - if (progress > total) return - progressSlider?.value = progress.toFloat() - + progressSlider?.value = + progress.toFloat().coerceIn(progressSlider?.valueFrom, progressSlider?.valueTo) } else { seekBar?.max = total diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt index 4186bad55..c98c4fdd6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt @@ -35,7 +35,6 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.databinding.FragmentCirclePlayerBinding import code.name.monkey.retromusic.extensions.* import code.name.monkey.retromusic.fragments.MusicSeekSkipTouchListener -import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.base.goToAlbum import code.name.monkey.retromusic.fragments.base.goToArtist @@ -315,16 +314,10 @@ class CirclePlayerFragment : AbsPlayerFragment(R.layout.fragment_circle_player), val progressSlider = binding.progressSlider progressSlider.valueTo = total.toFloat() - if (isSeeking) { - progressSlider.value = progress.toFloat() - } else { - progressAnimator = - ObjectAnimator.ofFloat(progressSlider, "value", progress.toFloat()).apply { - duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME - interpolator = LinearInterpolator() - start() - } - } + progressSlider.valueTo = total.toFloat() + + progressSlider.value = + progress.toFloat().coerceIn(progressSlider.valueFrom, progressSlider.valueTo) binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong()) binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong()) 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 048751906..e79bd784d 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 @@ -22,7 +22,6 @@ import android.graphics.PorterDuff import android.graphics.drawable.AnimatedVectorDrawable import android.os.Bundle import android.view.View -import android.view.animation.LinearInterpolator import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.Toolbar @@ -41,7 +40,6 @@ import code.name.monkey.retromusic.adapter.song.PlayingQueueAdapter import code.name.monkey.retromusic.databinding.FragmentGradientPlayerBinding import code.name.monkey.retromusic.extensions.* import code.name.monkey.retromusic.fragments.MusicSeekSkipTouchListener -import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.base.goToAlbum import code.name.monkey.retromusic.fragments.base.goToArtist @@ -573,16 +571,9 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play val progressSlider = binding.playbackControlsFragment.progressSlider progressSlider.valueTo = total.toFloat() - if (isSeeking) { - progressSlider.value = progress.toFloat() - } else { - progressAnimator = - ObjectAnimator.ofFloat(progressSlider, "value", progress.toFloat()).apply { - duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME - interpolator = LinearInterpolator() - start() - } - } + progressSlider.value = + progress.toFloat().coerceIn(progressSlider.valueFrom, progressSlider.valueTo) + binding.playbackControlsFragment.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong()) binding.playbackControlsFragment.songCurrentProgress.text = diff --git a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt index 009b0c40d..bdb58f3fe 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt @@ -19,7 +19,7 @@ class AudioFader { if (duration == 0) { return null } - return ValueAnimator.ofFloat(1f, 0f).apply { + return ValueAnimator.ofFloat(0f, 1f).apply { this.duration = duration.toLong() addUpdateListener { animation: ValueAnimator -> fadeInMp.setVolume( diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt index da27e6b36..64ba695a9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt @@ -303,7 +303,7 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) { private fun switchPlayer() { getNextPlayer()?.start() - crossFade(getCurrentPlayer()!!, getNextPlayer()!!) + crossFade(getNextPlayer()!!, getCurrentPlayer()!!) currentPlayer = if (currentPlayer == CurrentPlayer.PLAYER_ONE || currentPlayer == CurrentPlayer.NOT_SET) { CurrentPlayer.PLAYER_TWO From 8e3a7a097aa0dd1fe7637e4f5ccfca03456d672a Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 21:15:35 +0530 Subject: [PATCH 15/37] Decreased progress update interval for playing states --- .../helper/MusicProgressViewUpdateHelper.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 3562ef914..37deb038e 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 @@ -24,9 +24,10 @@ class MusicProgressViewUpdateHelper : Handler { private var callback: Callback? = null private var intervalPlaying: Int = 0 private var intervalPaused: Int = 0 + private var firstUpdate = true fun start() { - queueNextRefresh(1) + queueNextRefresh(refreshProgressViews().toLong()) } fun stop() { @@ -59,10 +60,11 @@ class MusicProgressViewUpdateHelper : Handler { private fun refreshProgressViews(): Int { val progressMillis = MusicPlayerRemote.songProgressMillis val totalMillis = MusicPlayerRemote.songDurationMillis - if (totalMillis > 0) + if (totalMillis > 0) { + firstUpdate = false callback?.onUpdateProgressViews(progressMillis, totalMillis) - - if (!MusicPlayerRemote.isPlaying) { + } + if (!MusicPlayerRemote.isPlaying && !firstUpdate) { return intervalPaused } @@ -84,7 +86,7 @@ class MusicProgressViewUpdateHelper : Handler { companion object { private const val CMD_REFRESH_PROGRESS_VIEWS = 1 private const val MIN_INTERVAL = 20 - private const val UPDATE_INTERVAL_PLAYING = 1000 + private const val UPDATE_INTERVAL_PLAYING = 500 private const val UPDATE_INTERVAL_PAUSED = 500 } } From df382cb539fe43d898d4db671bfcf10488641f8b Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 22:08:52 +0530 Subject: [PATCH 16/37] Fixed CrossFade not working when Fade Audio is enabled --- .../name/monkey/retromusic/service/AudioFader.kt | 15 ++++++--------- .../monkey/retromusic/service/CrossFadePlayer.kt | 2 +- .../monkey/retromusic/service/MusicService.kt | 9 ++++----- .../monkey/retromusic/service/PlaybackManager.kt | 11 +++++++---- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt index bdb58f3fe..3548a8c27 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt @@ -10,10 +10,10 @@ import code.name.monkey.retromusic.util.PreferenceUtil class AudioFader { companion object { - inline fun createFadeAnimator( + fun createFadeAnimator( fadeInMp: MediaPlayer, fadeOutMp: MediaPlayer, - crossinline endAction: (animator: Animator) -> Unit, /* Code to run when Animator Ends*/ + endAction: (animator: Animator) -> Unit, /* Code to run when Animator Ends*/ ): Animator? { val duration = PreferenceUtil.crossFadeDuration * 1000 if (duration == 0) { @@ -34,15 +34,14 @@ class AudioFader { } } - @JvmStatic fun startFadeAnimator( playback: Playback, fadeIn: Boolean, /* fadeIn -> true fadeOut -> false*/ - callback: Runnable, /* Code to run when Animator Ends*/ + callback: Runnable? = null, /* Code to run when Animator Ends*/ ) { val duration = PreferenceUtil.audioFadeDuration.toLong() if (duration == 0L) { - callback.run() + callback?.run() return } val startValue = if (fadeIn) 0f else 1.0f @@ -50,12 +49,10 @@ class AudioFader { val animator = ValueAnimator.ofFloat(startValue, endValue) animator.duration = duration animator.addUpdateListener { animation: ValueAnimator -> - playback.setVolume( - animation.animatedValue as Float - ) + playback.setVolume(animation.animatedValue as Float) } animator.doOnEnd { - callback.run() + callback?.run() } animator.start() } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt index 64ba695a9..7ce084f7c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt @@ -37,7 +37,7 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) { private var crossFadeAnimator: Animator? = null override var callbacks: PlaybackCallbacks? = null private var crossFadeDuration = PreferenceUtil.crossFadeDuration - private var isCrossFading = false + var isCrossFading = false init { player1.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index c45d9b83e..d1c274cc9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -770,11 +770,10 @@ class MusicService : MediaBrowserServiceCompat(), @Synchronized fun play() { - playbackManager.play(onNotInitialized = { playSongAt(getPosition()) }) { - if (notHandledMetaChangedForCurrentTrack) { - handleChangeInternal(META_CHANGED) - notHandledMetaChangedForCurrentTrack = false - } + playbackManager.play { playSongAt(getPosition()) } + if (notHandledMetaChangedForCurrentTrack) { + handleChangeInternal(META_CHANGED) + notHandledMetaChangedForCurrentTrack = false } notifyChange(PLAY_STATE_CHANGED) } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt index 983968044..83fa7302b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt @@ -47,16 +47,19 @@ class PlaybackManager(val context: Context) { playback?.callbacks = callbacks } - fun play(onNotInitialized: () -> Unit = {}, onPlay: () -> Unit = {}) { + fun play(onNotInitialized: () -> Unit) { if (playback != null && !playback!!.isPlaying) { if (!playback!!.isInitialized) { onNotInitialized() } else { openAudioEffectSession() if (playbackLocation == PlaybackLocation.LOCAL) { - AudioFader.startFadeAnimator(playback!!, true) { - // Code when Animator Ends - onPlay() + if (playback is CrossFadePlayer) { + if (!(playback as CrossFadePlayer).isCrossFading) { + AudioFader.startFadeAnimator(playback!!, true) + } + } else { + AudioFader.startFadeAnimator(playback!!, true) } } if (shouldSetSpeed) { From 3dc26974b4cdc03b9f3162e00890504f744d2676 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 12 Jun 2022 22:09:09 +0530 Subject: [PATCH 17/37] Update change-log --- app/src/main/assets/retro-changelog.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html index 45bbf979d..18f5605c2 100644 --- a/app/src/main/assets/retro-changelog.html +++ b/app/src/main/assets/retro-changelog.html @@ -68,6 +68,9 @@

Fixed

  • Fixed ChromeCast crash
  • +
  • Fixed Slider crashes
  • +
  • Fixed storage related crashes on Android 10
  • +
  • Fixed CrossFade not working Fade Audio is not working
From 2a5e6d775699bf3bb7317ce361ea63a6be45102e Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Tue, 14 Jun 2022 23:05:59 +0530 Subject: [PATCH 18/37] Added F-Droid FOSS flavor --- app/build.gradle | 21 ++- .../activities/base/AbsCastActivity.kt | 5 + .../retromusic/billing/BillingManager.kt | 9 + .../monkey/retromusic/cast/RetroWebServer.kt | 6 + .../retromusic/extensions/extensions.kt | 15 ++ .../monkey/retromusic/service/CastPlayer.kt | 47 +++++ .../name/monkey/retromusic/util/AppRater.kt | 8 + app/src/main/AndroidManifest.xml | 1 - .../java/code/name/monkey/retromusic/App.kt | 30 +--- .../activities/SupportDevelopmentActivity.kt | 170 +----------------- .../activities/base/AbsThemeActivity.kt | 3 +- .../retromusic/dialogs/SleepTimerDialog.kt | 2 +- .../fragments/albums/AlbumsFragment.kt | 4 +- .../fragments/artists/ArtistsFragment.kt | 4 +- .../fragments/genres/GenresFragment.kt | 4 +- .../retromusic/fragments/home/HomeFragment.kt | 8 +- .../fragments/library/LibraryFragment.kt | 4 +- .../fragments/playlists/PlaylistsFragment.kt | 4 +- .../fragments/settings/AbsSettingsFragment.kt | 3 +- .../settings/MainSettingsFragment.kt | 5 +- .../settings/OtherSettingsFragment.kt | 20 +-- .../fragments/songs/SongsFragment.kt | 4 +- .../retromusic/helper/MusicPlayerRemote.kt | 6 +- .../AlbumCoverStylePreferenceDialog.kt | 2 +- .../NowPlayingScreenPreferenceDialog.kt | 2 +- .../service/MediaButtonIntentReceiver.kt | 5 +- .../monkey/retromusic/service/MusicService.kt | 5 +- .../retromusic/service/PlaybackManager.kt | 5 +- .../monkey/retromusic/util/NavigationUtil.kt | 6 - .../monkey/retromusic/util/PremiumShow.kt | 33 ---- app/src/main/res/layout/activity_donation.xml | 48 ----- app/src/normal/AndroidManifest.xml | 9 + .../retromusic/activities/PurchaseActivity.kt | 20 +-- .../activities/base/AbsCastActivity.kt | 5 +- .../retromusic/billing/BillingManager.kt | 37 ++++ .../name/monkey/retromusic/cast/CastHelper.kt | 0 .../retromusic/cast/CastOptionsProvider.kt | 0 .../cast/RetroSessionManagerListener.kt | 0 .../monkey/retromusic/cast/RetroWebServer.kt | 0 .../retromusic/extensions/extensions.kt | 42 +++++ .../monkey/retromusic/service/CastPlayer.kt | 0 .../name/monkey/retromusic/util/AppRater.kt | 1 - 42 files changed, 243 insertions(+), 360 deletions(-) create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/cast/RetroWebServer.kt create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/extensions/extensions.kt create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/service/CastPlayer.kt create mode 100644 app/src/fdroid/java/code/name/monkey/retromusic/util/AppRater.kt delete mode 100644 app/src/main/java/code/name/monkey/retromusic/util/PremiumShow.kt create mode 100644 app/src/normal/AndroidManifest.xml rename app/src/{main => normal}/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt (82%) rename app/src/{main => normal}/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt (93%) create mode 100644 app/src/normal/java/code/name/monkey/retromusic/billing/BillingManager.kt rename app/src/{main => normal}/java/code/name/monkey/retromusic/cast/CastHelper.kt (100%) rename app/src/{main => normal}/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt (100%) rename app/src/{main => normal}/java/code/name/monkey/retromusic/cast/RetroSessionManagerListener.kt (100%) rename app/src/{main => normal}/java/code/name/monkey/retromusic/cast/RetroWebServer.kt (100%) create mode 100644 app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt rename app/src/{main => normal}/java/code/name/monkey/retromusic/service/CastPlayer.kt (100%) rename app/src/{main => normal}/java/code/name/monkey/retromusic/util/AppRater.kt (99%) diff --git a/app/build.gradle b/app/build.gradle index f92862ea9..04138bfcf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,15 @@ android { versionNameSuffix ' DEBUG' } } + flavorDimensions "version" + productFlavors { + normal { + dimension "version" + } + fdroid { + dimension "version" + } + } buildFeatures{ viewBinding true @@ -95,11 +104,11 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.palette:palette-ktx:1.0.0' - //Cast Dependencies implementation 'androidx.mediarouter:mediarouter:1.3.0' - implementation 'com.google.android.gms:play-services-cast-framework:21.0.1' + //Cast Dependencies + normalImplementation 'com.google.android.gms:play-services-cast-framework:21.0.1' //WebServer by NanoHttpd - implementation "org.nanohttpd:nanohttpd:2.3.1" + normalImplementation "org.nanohttpd:nanohttpd:2.3.1" implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" @@ -117,8 +126,8 @@ dependencies { implementation "androidx.core:core-splashscreen:1.0.0-rc01" - implementation 'com.google.android.play:feature-delivery:2.0.0' - implementation 'com.google.android.play:review:2.0.0' + normalImplementation 'com.google.android.play:feature-delivery:2.0.0' + normalImplementation 'com.google.android.play:review:2.0.0' implementation "com.google.android.material:material:$mdc_version" @@ -160,7 +169,7 @@ dependencies { implementation 'org.eclipse.mylyn.github:org.eclipse.egit.github.core:2.1.5' implementation 'com.github.Adonai:jaudiotagger:2.3.15' - implementation 'com.anjlab.android.iab.v3:library:2.0.3' + normalImplementation 'com.anjlab.android.iab.v3:library:2.0.3' implementation 'com.r0adkll:slidableactivity:2.1.0' implementation 'com.heinrichreimersoftware:material-intro:2.0.0' implementation 'com.github.dhaval2404:imagepicker:2.1' diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt b/app/src/fdroid/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt new file mode 100644 index 000000000..def9a7050 --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt @@ -0,0 +1,5 @@ +package code.name.monkey.retromusic.activities.base + + +abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() { +} \ No newline at end of file diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt b/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt new file mode 100644 index 000000000..fa2402ef0 --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt @@ -0,0 +1,9 @@ +package code.name.monkey.retromusic.billing + +class BillingManager { + + fun release() {} + + val isProVersion: Boolean + get() = true +} \ No newline at end of file diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/cast/RetroWebServer.kt b/app/src/fdroid/java/code/name/monkey/retromusic/cast/RetroWebServer.kt new file mode 100644 index 000000000..03103dfb7 --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/cast/RetroWebServer.kt @@ -0,0 +1,6 @@ +package code.name.monkey.retromusic.cast + +import android.content.Context + +@Suppress("UNUSED_PARAMETER") +class RetroWebServer(context: Context) \ No newline at end of file diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/extensions/extensions.kt b/app/src/fdroid/java/code/name/monkey/retromusic/extensions/extensions.kt new file mode 100644 index 000000000..3d93f388c --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/extensions/extensions.kt @@ -0,0 +1,15 @@ +@file:Suppress("UNUSED_PARAMETER", "unused") + +package code.name.monkey.retromusic.extensions + +import android.content.Context +import android.view.Menu +import androidx.fragment.app.FragmentActivity + +fun Context.setUpMediaRouteButton(menu: Menu) {} + +fun FragmentActivity.installLanguageAndRecreate(code: String) {} + +fun Context.goToProVersion() {} + +fun Context.installSplitCompat() {} \ No newline at end of file diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/service/CastPlayer.kt b/app/src/fdroid/java/code/name/monkey/retromusic/service/CastPlayer.kt new file mode 100644 index 000000000..3a0eaca28 --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/service/CastPlayer.kt @@ -0,0 +1,47 @@ +package code.name.monkey.retromusic.service + +import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.service.playback.Playback + +// Empty CastPlayer implementation +class CastPlayer : Playback { + override val isInitialized: Boolean + get() = true + override val isPlaying: Boolean + get() = true + override val audioSessionId: Int + get() = 0 + + override fun setDataSource( + song: Song, + force: Boolean, + completion: (success: Boolean) -> Unit, + ) { + } + + override fun setNextDataSource(path: String?) {} + + override var callbacks: Playback.PlaybackCallbacks? = null + + override fun start() = true + + override fun stop() {} + + override fun release() {} + + override fun pause(): Boolean = true + + override fun duration() = 0 + + override fun position() = 0 + + override fun seek(whereto: Int) = whereto + + override fun setVolume(vol: Float) = true + + override fun setAudioSessionId(sessionId: Int) = true + + override fun setCrossFadeDuration(duration: Int) {} + + override fun setPlaybackSpeedPitch(speed: Float, pitch: Float) {} +} \ No newline at end of file diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/util/AppRater.kt b/app/src/fdroid/java/code/name/monkey/retromusic/util/AppRater.kt new file mode 100644 index 000000000..2691d70b2 --- /dev/null +++ b/app/src/fdroid/java/code/name/monkey/retromusic/util/AppRater.kt @@ -0,0 +1,8 @@ +package code.name.monkey.retromusic.util + +import android.content.Context + +@Suppress("UNUSED_PARAMETER") +object AppRater { + fun appLaunched(context: Context) {} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efbc081f6..b8eb2170a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -120,7 +120,6 @@ - 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 f92442df4..b60870b52 100644 --- a/app/src/main/java/code/name/monkey/retromusic/App.kt +++ b/app/src/main/java/code/name/monkey/retromusic/App.kt @@ -19,20 +19,17 @@ import androidx.preference.PreferenceManager import cat.ereza.customactivityoncrash.config.CaocConfig import code.name.monkey.appthemehelper.ThemeStore import code.name.monkey.appthemehelper.util.VersionUtils -import code.name.monkey.retromusic.Constants.PRO_VERSION_PRODUCT_ID import code.name.monkey.retromusic.activities.ErrorActivity import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appshortcuts.DynamicShortcutManager -import code.name.monkey.retromusic.extensions.showToast +import code.name.monkey.retromusic.billing.BillingManager import code.name.monkey.retromusic.helper.WallpaperAccentManager -import com.anjlab.android.iab.v3.BillingProcessor -import com.anjlab.android.iab.v3.PurchaseInfo import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin class App : Application() { - lateinit var billingProcessor: BillingProcessor + lateinit var billingManager: BillingManager private val wallpaperAccentManager = WallpaperAccentManager(this) override fun onCreate() { @@ -55,33 +52,18 @@ class App : Application() { if (VersionUtils.hasNougatMR()) DynamicShortcutManager(this).initDynamicShortcuts() - // automatically restores purchases - billingProcessor = BillingProcessor( - this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, - object : BillingProcessor.IBillingHandler { - override fun onProductPurchased(productId: String, details: PurchaseInfo?) {} - - override fun onPurchaseHistoryRestored() { - showToast(R.string.restored_previous_purchase_please_restart) - } - - override fun onBillingError(errorCode: Int, error: Throwable?) {} - - override fun onBillingInitialized() {} - }) - // setting Error activity CaocConfig.Builder.create().errorActivity(ErrorActivity::class.java) .restartActivity(MainActivity::class.java).apply() // Set Default values for now playing preferences - // This will reduce start time for now playing settings fragment as Preference listener of AbsSlidingMusicPanelActivity won't be called + // This will reduce startup time for now playing settings fragment as Preference listener of AbsSlidingMusicPanelActivity won't be called PreferenceManager.setDefaultValues(this, R.xml.pref_now_playing_screen, false) } override fun onTerminate() { super.onTerminate() - billingProcessor.release() + billingManager.release() wallpaperAccentManager.release() } @@ -93,9 +75,7 @@ class App : Application() { } fun isProVersion(): Boolean { - return BuildConfig.DEBUG || instance?.billingProcessor!!.isPurchased( - PRO_VERSION_PRODUCT_ID - ) + return BuildConfig.DEBUG || instance?.billingManager!!.isProVersion } } } 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 377cbec93..e0d67a21f 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 @@ -14,41 +14,23 @@ */ package code.name.monkey.retromusic.activities -import android.graphics.Paint import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater import android.view.MenuItem -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import code.name.monkey.appthemehelper.util.ATHUtil -import code.name.monkey.appthemehelper.util.TintHelper import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper -import code.name.monkey.retromusic.BuildConfig -import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsThemeActivity import code.name.monkey.retromusic.databinding.ActivityDonationBinding -import code.name.monkey.retromusic.databinding.ItemDonationOptionBinding -import code.name.monkey.retromusic.extensions.* -import com.anjlab.android.iab.v3.BillingProcessor -import com.anjlab.android.iab.v3.PurchaseInfo -import com.anjlab.android.iab.v3.SkuDetails +import code.name.monkey.retromusic.extensions.setStatusBarColorAuto +import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto +import code.name.monkey.retromusic.extensions.surfaceColor -class SupportDevelopmentActivity : AbsThemeActivity(), BillingProcessor.IBillingHandler { +class SupportDevelopmentActivity : AbsThemeActivity() { lateinit var binding: ActivityDonationBinding companion object { val TAG: String = SupportDevelopmentActivity::class.java.simpleName - const val DONATION_PRODUCT_IDS = R.array.donation_ids } - var billingProcessor: BillingProcessor? = null - override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { onBackPressed() @@ -57,11 +39,6 @@ class SupportDevelopmentActivity : AbsThemeActivity(), BillingProcessor.IBilling return super.onOptionsItemSelected(item) } - fun donate(i: Int) { - val ids = resources.getStringArray(DONATION_PRODUCT_IDS) - billingProcessor?.purchase(this, ids[i]) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityDonationBinding.inflate(layoutInflater) @@ -72,148 +49,11 @@ class SupportDevelopmentActivity : AbsThemeActivity(), BillingProcessor.IBilling setupToolbar() - billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, this) - TintHelper.setTint(binding.progress, accentColor()) - binding.donation.setTextColor(accentColor()) } private fun setupToolbar() { - val toolbarColor = surfaceColor() - binding.toolbar.setBackgroundColor(toolbarColor) + binding.toolbar.setBackgroundColor(surfaceColor()) ToolbarContentTintHelper.colorBackButton(binding.toolbar) setSupportActionBar(binding.toolbar) } - - override fun onBillingInitialized() { - loadSkuDetails() - } - - private fun loadSkuDetails() { - binding.progressContainer.isVisible = true - binding.recyclerView.isVisible = false - val ids = resources.getStringArray(DONATION_PRODUCT_IDS) - billingProcessor!!.getPurchaseListingDetailsAsync( - ArrayList(listOf(*ids)), - object : BillingProcessor.ISkuDetailsResponseListener { - override fun onSkuDetailsResponse(skuDetails: MutableList?) { - if (skuDetails == null || skuDetails.isEmpty()) { - binding.progressContainer.isVisible = false - return - } - - binding.progressContainer.isVisible = false - binding.recyclerView.apply { - itemAnimator = DefaultItemAnimator() - layoutManager = GridLayoutManager(this@SupportDevelopmentActivity, 2) - adapter = SkuDetailsAdapter(this@SupportDevelopmentActivity, skuDetails) - isVisible = true - } - } - - override fun onSkuDetailsError(error: String?) { - Log.e(TAG, error.toString()) - } - }) - } - - override fun onProductPurchased(productId: String, details: PurchaseInfo?) { - // loadSkuDetails(); - showToast(R.string.thank_you) - } - - override fun onBillingError(errorCode: Int, error: Throwable?) { - Log.e(TAG, "Billing error: code = $errorCode", error) - } - - override fun onPurchaseHistoryRestored() { - // loadSkuDetails(); - showToast(R.string.restored_previous_purchases) - } - - override fun onDestroy() { - billingProcessor?.release() - super.onDestroy() - } -} - -class SkuDetailsAdapter( - private var donationsDialog: SupportDevelopmentActivity, - objects: List, -) : RecyclerView.Adapter() { - - private var skuDetailsList: List = ArrayList() - - init { - skuDetailsList = objects - } - - private fun getIcon(position: Int): Int { - return when (position) { - 0 -> R.drawable.ic_cookie - 1 -> R.drawable.ic_take_away - 2 -> R.drawable.ic_take_away_coffe - 3 -> R.drawable.ic_beer - 4 -> R.drawable.ic_fast_food_meal - 5 -> R.drawable.ic_popcorn - 6 -> R.drawable.ic_card_giftcard - 7 -> R.drawable.ic_food_croissant - else -> R.drawable.ic_card_giftcard - } - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { - return ViewHolder( - ItemDonationOptionBinding.inflate( - LayoutInflater.from(donationsDialog), - viewGroup, - false - ) - ) - } - - override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val skuDetails = skuDetailsList[i] - with(viewHolder.binding) { - itemTitle.text = skuDetails.title.replace("(Retro Music Player MP3 Player)", "") - .trim { it <= ' ' } - itemText.text = skuDetails.description - itemText.isVisible = false - itemPrice.text = skuDetails.priceText - itemImage.setImageResource(getIcon(i)) - } - - val purchased = donationsDialog.billingProcessor!!.isPurchased(skuDetails.productId) - val titleTextColor = if (purchased) ATHUtil.resolveColor( - donationsDialog, - android.R.attr.textColorHint - ) else donationsDialog.textColorPrimary() - val contentTextColor = - if (purchased) titleTextColor else donationsDialog.textColorSecondary() - - with(viewHolder.binding) { - itemTitle.setTextColor(titleTextColor) - itemText.setTextColor(contentTextColor) - itemPrice.setTextColor(titleTextColor) - strikeThrough(itemTitle, purchased) - strikeThrough(itemText, purchased) - strikeThrough(itemPrice, purchased) - } - - viewHolder.itemView.isEnabled = !purchased - viewHolder.itemView.setOnClickListener { donationsDialog.donate(i) } - } - - override fun getItemCount(): Int { - return skuDetailsList.size - } - - class ViewHolder(val binding: ItemDonationOptionBinding) : RecyclerView.ViewHolder(binding.root) - - companion object { - private fun strikeThrough(textView: TextView, strikeThrough: Boolean) { - textView.paintFlags = - if (strikeThrough) textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG - else textView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() - } - } } 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 772a8cc38..ca112a6d4 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 @@ -31,7 +31,6 @@ import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.maybeShowAnnoyingToasts import code.name.monkey.retromusic.util.theme.getNightMode import code.name.monkey.retromusic.util.theme.getThemeResValue -import com.google.android.play.core.splitcompat.SplitCompat import java.util.* abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable { @@ -105,6 +104,6 @@ abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable { Locale.forLanguageTag(code) } super.attachBaseContext(LanguageContextWrapper.wrap(newBase, locale)) - SplitCompat.install(this) + installSplitCompat() } } 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 6a44e4638..32de5ba6c 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 @@ -95,7 +95,7 @@ class SleepTimerDialog : DialogFragment() { shouldFinishLastSong.isVisible = false timerUpdater.start() setPositiveButton(android.R.string.ok, null) - setNegativeButton(R.string.cast_stop) { _, _ -> + setNegativeButton(R.string.action_cancel) { _, _ -> timerUpdater.cancel() val previous = makeTimerPendingIntent(PendingIntent.FLAG_NO_CREATE) if (previous != null) { 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 55a8bb88d..ed00699a5 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 @@ -24,6 +24,7 @@ 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.setUpMediaRouteButton import code.name.monkey.retromusic.extensions.surfaceColor import code.name.monkey.retromusic.fragments.GridStyle import code.name.monkey.retromusic.fragments.ReloadType @@ -41,7 +42,6 @@ import com.afollestad.materialcab.attached.AttachedCab import com.afollestad.materialcab.attached.destroy import com.afollestad.materialcab.attached.isActive import com.afollestad.materialcab.createCab -import com.google.android.gms.cast.framework.CastButtonFactory class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), IAlbumClickListener, ICabHolder { @@ -169,7 +169,7 @@ class AlbumsFragment : AbsRecyclerViewCustomGridSizeFragment(), IArtistClickListener, IAlbumArtistClickListener, ICabHolder { @@ -180,7 +180,7 @@ class ArtistsFragment : AbsRecyclerViewCustomGridSizeFragment(), menu.removeItem(R.id.action_sort_order) menu.findItem(R.id.action_settings).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) //Setting up cast button - CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.action_cast) + requireContext().setUpMediaRouteButton(menu) } override fun onResume() { 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 6d42e2801..4a915690e 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 @@ -35,10 +35,7 @@ import code.name.monkey.retromusic.adapter.HomeAdapter import code.name.monkey.retromusic.databinding.FragmentHomeBinding import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog -import code.name.monkey.retromusic.extensions.accentColor -import code.name.monkey.retromusic.extensions.dip -import code.name.monkey.retromusic.extensions.drawNextToNavbar -import code.name.monkey.retromusic.extensions.elevatedAccentColor +import code.name.monkey.retromusic.extensions.* import code.name.monkey.retromusic.fragments.ReloadType import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.glide.GlideApp @@ -48,7 +45,6 @@ import code.name.monkey.retromusic.interfaces.IScrollHelper import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil.userName -import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialSharedAxis @@ -212,7 +208,7 @@ class HomeFragment : ATHToolbarActivity.getToolbarBackgroundColor(binding.toolbar) ) //Setting up cast button - CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.action_cast) + requireContext().setUpMediaRouteButton(menu) } override fun scrollToTop() { 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 eb66ca5b6..c5b02c973 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 @@ -30,11 +30,11 @@ import code.name.monkey.retromusic.R import code.name.monkey.retromusic.databinding.FragmentLibraryBinding import code.name.monkey.retromusic.dialogs.CreatePlaylistDialog import code.name.monkey.retromusic.dialogs.ImportPlaylistDialog +import code.name.monkey.retromusic.extensions.setUpMediaRouteButton import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment import code.name.monkey.retromusic.model.CategoryInfo import code.name.monkey.retromusic.util.PreferenceUtil -import com.google.android.gms.cast.framework.CastButtonFactory class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { @@ -99,7 +99,7 @@ class LibraryFragment : AbsMainActivityFragment(R.layout.fragment_library) { getToolbarBackgroundColor(binding.toolbar) ) //Setting up cast button - CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.action_cast) + requireContext().setUpMediaRouteButton(menu) } override fun onMenuItemSelected(item: MenuItem): Boolean { 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 2c212d039..bf74cee56 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 @@ -24,13 +24,13 @@ 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.db.PlaylistWithSongs +import code.name.monkey.retromusic.extensions.setUpMediaRouteButton 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 code.name.monkey.retromusic.util.RetroUtil -import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.material.transition.MaterialSharedAxis class PlaylistsFragment : @@ -85,7 +85,7 @@ class PlaylistsFragment : setUpSortOrderMenu(menu.findItem(R.id.action_sort_order).subMenu) MenuCompat.setGroupDividerEnabled(menu, true) //Setting up cast button - CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.action_cast) + requireContext().setUpMediaRouteButton(menu) } override fun onMenuItemSelected(item: MenuItem): Boolean { 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 af3e4a74b..b3688f609 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 @@ -26,6 +26,7 @@ import androidx.preference.PreferenceManager import code.name.monkey.appthemehelper.common.prefs.supportv7.ATEPreferenceFragmentCompat import code.name.monkey.retromusic.R import code.name.monkey.retromusic.extensions.dip +import code.name.monkey.retromusic.extensions.goToProVersion import code.name.monkey.retromusic.extensions.showToast import code.name.monkey.retromusic.preferences.* import code.name.monkey.retromusic.util.NavigationUtil @@ -39,7 +40,7 @@ abstract class AbsSettingsFragment : ATEPreferenceFragmentCompat() { internal fun showProToastAndNavigate(message: String) { showToast(getString(R.string.message_pro_feature, message)) - NavigationUtil.goToProVersion(requireActivity()) + requireContext().goToProVersion() } internal fun setSummary(preference: Preference, value: Any?) { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/MainSettingsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/MainSettingsFragment.kt index aff3f4779..fa58c0bd2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/settings/MainSettingsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/settings/MainSettingsFragment.kt @@ -27,6 +27,7 @@ import code.name.monkey.retromusic.App import code.name.monkey.retromusic.R import code.name.monkey.retromusic.databinding.FragmentMainSettingsBinding import code.name.monkey.retromusic.extensions.drawAboveSystemBarsWithPadding +import code.name.monkey.retromusic.extensions.goToProVersion import code.name.monkey.retromusic.util.NavigationUtil class MainSettingsFragment : Fragment(), View.OnClickListener { @@ -77,11 +78,11 @@ class MainSettingsFragment : Fragment(), View.OnClickListener { binding.buyProContainer.apply { isGone = App.isProVersion() setOnClickListener { - NavigationUtil.goToProVersion(requireContext()) + requireContext().goToProVersion() } } binding.buyPremium.setOnClickListener { - NavigationUtil.goToProVersion(requireContext()) + requireContext().goToProVersion() } ThemeStore.accentColor(requireContext()).let { binding.buyPremium.setTextColor(it) 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 9164b3156..554bc674a 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 @@ -21,12 +21,10 @@ 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.extensions.installLanguageAndRecreate import code.name.monkey.retromusic.fragments.LibraryViewModel import code.name.monkey.retromusic.fragments.ReloadType.HomeSections -import com.google.android.play.core.splitinstall.SplitInstallManagerFactory -import com.google.android.play.core.splitinstall.SplitInstallRequest import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import java.util.* /** * @author Hemanth S (h4h13). @@ -58,21 +56,7 @@ class OtherSettingsFragment : AbsSettingsFragment() { val languagePreference: Preference? = findPreference(LANGUAGE_NAME) languagePreference?.setOnPreferenceChangeListener { prefs, newValue -> setSummary(prefs, newValue) - val code = newValue.toString() - val manager = SplitInstallManagerFactory.create(requireContext()) - if (code != "auto") { - // Try to download language resources - val request = - SplitInstallRequest.newBuilder().addLanguage(Locale.forLanguageTag(code)) - .build() - manager.startInstall(request) - // Recreate the activity on download complete - .addOnCompleteListener { - restartActivity() - } - } else { - requireActivity().recreate() - } + requireActivity().installLanguageAndRecreate(newValue.toString()) true } } 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 a728e2828..e47efaa1d 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 @@ -21,6 +21,7 @@ import androidx.annotation.LayoutRes import androidx.recyclerview.widget.GridLayoutManager import code.name.monkey.retromusic.R import code.name.monkey.retromusic.adapter.song.SongAdapter +import code.name.monkey.retromusic.extensions.setUpMediaRouteButton import code.name.monkey.retromusic.extensions.surfaceColor import code.name.monkey.retromusic.fragments.GridStyle import code.name.monkey.retromusic.fragments.ReloadType @@ -35,7 +36,6 @@ import com.afollestad.materialcab.attached.AttachedCab import com.afollestad.materialcab.attached.destroy import com.afollestad.materialcab.attached.isActive import com.afollestad.materialcab.createCab -import com.google.android.gms.cast.framework.CastButtonFactory class SongsFragment : AbsRecyclerViewCustomGridSizeFragment(), ICabHolder { @@ -136,7 +136,7 @@ class SongsFragment : AbsRecyclerViewCustomGridSizeFragment Unit, ) { playbackLocation = PlaybackLocation.REMOTE - switchToPlayback(CastPlayer(castSession), onChange) + switchToPlayback(castPlayer, onChange) } private fun switchToPlayback( diff --git a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.kt index 2d960af00..bea599a00 100755 --- a/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/NavigationUtil.kt @@ -39,12 +39,6 @@ object NavigationUtil { ) } - fun goToProVersion(context: Context) { - context.startActivity( - Intent(context, PurchaseActivity::class.java), null - ) - } - fun goToSupportDevelopment(activity: Activity) { activity.startActivity( Intent(activity, SupportDevelopmentActivity::class.java), null diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PremiumShow.kt b/app/src/main/java/code/name/monkey/retromusic/util/PremiumShow.kt deleted file mode 100644 index adcf36971..000000000 --- a/app/src/main/java/code/name/monkey/retromusic/util/PremiumShow.kt +++ /dev/null @@ -1,33 +0,0 @@ -package code.name.monkey.retromusic.util - -import android.content.Context -import android.content.Intent -import code.name.monkey.retromusic.App -import code.name.monkey.retromusic.activities.PurchaseActivity - -object PremiumShow { - private const val PREF_NAME = "premium_show" - private const val LAUNCH_COUNT = "launch_count" - private const val DATE_FIRST_LAUNCH = "date_first_launch" - - @JvmStatic - fun launch(context: Context) { - val pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - if (App.isProVersion()) { - return - } - val prefEditor = pref.edit() - val launchCount = pref.getLong(LAUNCH_COUNT, 0) + 1 - prefEditor.putLong(LAUNCH_COUNT, launchCount) - - var dateLaunched = pref.getLong(DATE_FIRST_LAUNCH, 0) - if (dateLaunched == 0L) { - dateLaunched = System.currentTimeMillis() - prefEditor.putLong(DATE_FIRST_LAUNCH, dateLaunched) - } - if (System.currentTimeMillis() >= dateLaunched + 2 * 24 * 60 * 60 * 1000) { - context.startActivity(Intent(context, PurchaseActivity::class.java), null) - } - prefEditor.apply() - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_donation.xml b/app/src/main/res/layout/activity_donation.xml index 9f2db5c0c..c69fa30cf 100644 --- a/app/src/main/res/layout/activity_donation.xml +++ b/app/src/main/res/layout/activity_donation.xml @@ -1,7 +1,6 @@ @@ -31,52 +30,5 @@ android:overScrollMode="@integer/overScrollMode" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/normal/AndroidManifest.xml b/app/src/normal/AndroidManifest.xml new file mode 100644 index 000000000..855ade12b --- /dev/null +++ b/app/src/normal/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt b/app/src/normal/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt similarity index 82% rename from app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt rename to app/src/normal/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt index accf2f880..b23e1d62d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt +++ b/app/src/normal/java/code/name/monkey/retromusic/activities/PurchaseActivity.kt @@ -1,17 +1,3 @@ -/* - * 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 @@ -22,7 +8,7 @@ import android.view.MenuItem import code.name.monkey.appthemehelper.util.MaterialUtil import code.name.monkey.retromusic.App import code.name.monkey.retromusic.BuildConfig -import code.name.monkey.retromusic.Constants.PRO_VERSION_PRODUCT_ID +import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsThemeActivity import code.name.monkey.retromusic.databinding.ActivityProVersionBinding @@ -58,7 +44,7 @@ class PurchaseActivity : AbsThemeActivity(), BillingProcessor.IBillingHandler { restorePurchase() } binding.purchaseButton.setOnClickListener { - billingProcessor.purchase(this@PurchaseActivity, PRO_VERSION_PRODUCT_ID) + billingProcessor.purchase(this@PurchaseActivity, Constants.PRO_VERSION_PRODUCT_ID) } binding.bannerContainer.backgroundTintList = ColorStateList.valueOf(accentColor()) @@ -116,4 +102,4 @@ class PurchaseActivity : AbsThemeActivity(), BillingProcessor.IBillingHandler { companion object { private const val TAG: String = "PurchaseActivity" } -} +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt b/app/src/normal/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt similarity index 93% rename from app/src/main/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt rename to app/src/normal/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt index 23d668a53..4a2f3aaea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt +++ b/app/src/normal/java/code/name/monkey/retromusic/activities/base/AbsCastActivity.kt @@ -3,6 +3,7 @@ package code.name.monkey.retromusic.activities.base import code.name.monkey.retromusic.cast.RetroSessionManagerListener import code.name.monkey.retromusic.cast.RetroWebServer import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.service.CastPlayer import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.common.ConnectionResult @@ -37,7 +38,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() { override fun onSessionStarted(castSession: CastSession, p1: String) { invalidateOptionsMenu() mCastSession = castSession - MusicPlayerRemote.switchToRemotePlayback(castSession) + MusicPlayerRemote.switchToRemotePlayback(CastPlayer(castSession)) } override fun onSessionEnded(castSession: CastSession, p1: Int) { @@ -53,7 +54,7 @@ abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() { invalidateOptionsMenu() mCastSession = castSession webServer.start() - MusicPlayerRemote.switchToRemotePlayback(castSession) + MusicPlayerRemote.switchToRemotePlayback(CastPlayer(castSession)) } override fun onSessionSuspended(castSession: CastSession, p1: Int) { diff --git a/app/src/normal/java/code/name/monkey/retromusic/billing/BillingManager.kt b/app/src/normal/java/code/name/monkey/retromusic/billing/BillingManager.kt new file mode 100644 index 000000000..445044e73 --- /dev/null +++ b/app/src/normal/java/code/name/monkey/retromusic/billing/BillingManager.kt @@ -0,0 +1,37 @@ +package code.name.monkey.retromusic.billing + +import android.content.Context +import code.name.monkey.retromusic.BuildConfig +import code.name.monkey.retromusic.Constants +import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.extensions.showToast +import com.anjlab.android.iab.v3.BillingProcessor +import com.anjlab.android.iab.v3.PurchaseInfo + +class BillingManager(context: Context) { + private val billingProcessor: BillingProcessor + + init { + // automatically restores purchases + billingProcessor = BillingProcessor( + context, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, + object : BillingProcessor.IBillingHandler { + override fun onProductPurchased(productId: String, details: PurchaseInfo?) {} + + override fun onPurchaseHistoryRestored() { + context.showToast(R.string.restored_previous_purchase_please_restart) + } + + override fun onBillingError(errorCode: Int, error: Throwable?) {} + + override fun onBillingInitialized() {} + }) + } + + fun release() { + billingProcessor.release() + } + + val isProVersion: Boolean + get() = billingProcessor.isPurchased(Constants.PRO_VERSION_PRODUCT_ID) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/CastHelper.kt b/app/src/normal/java/code/name/monkey/retromusic/cast/CastHelper.kt similarity index 100% rename from app/src/main/java/code/name/monkey/retromusic/cast/CastHelper.kt rename to app/src/normal/java/code/name/monkey/retromusic/cast/CastHelper.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt b/app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt similarity index 100% rename from app/src/main/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt rename to app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/RetroSessionManagerListener.kt b/app/src/normal/java/code/name/monkey/retromusic/cast/RetroSessionManagerListener.kt similarity index 100% rename from app/src/main/java/code/name/monkey/retromusic/cast/RetroSessionManagerListener.kt rename to app/src/normal/java/code/name/monkey/retromusic/cast/RetroSessionManagerListener.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/RetroWebServer.kt b/app/src/normal/java/code/name/monkey/retromusic/cast/RetroWebServer.kt similarity index 100% rename from app/src/main/java/code/name/monkey/retromusic/cast/RetroWebServer.kt rename to app/src/normal/java/code/name/monkey/retromusic/cast/RetroWebServer.kt diff --git a/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt b/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt new file mode 100644 index 000000000..3ac58980f --- /dev/null +++ b/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt @@ -0,0 +1,42 @@ +package code.name.monkey.retromusic.extensions + +import android.content.Context +import android.content.Intent +import android.view.Menu +import androidx.fragment.app.FragmentActivity +import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.PurchaseActivity +import com.google.android.gms.cast.framework.CastButtonFactory +import com.google.android.play.core.splitcompat.SplitCompat +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory +import com.google.android.play.core.splitinstall.SplitInstallRequest +import java.util.* + +fun Context.setUpMediaRouteButton(menu: Menu) { + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.action_cast) +} + +fun FragmentActivity.installLanguageAndRecreate(code: String) { + val manager = SplitInstallManagerFactory.create(this) + if (code != "auto") { + // Try to download language resources + val request = + SplitInstallRequest.newBuilder().addLanguage(Locale.forLanguageTag(code)) + .build() + manager.startInstall(request) + // Recreate the activity on download complete + .addOnCompleteListener { + recreate() + } + } else { + recreate() + } +} + +fun Context.goToProVersion() { + startActivity(Intent(this, PurchaseActivity::class.java)) +} + +fun Context.installSplitCompat() { + SplitCompat.install(this) +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt b/app/src/normal/java/code/name/monkey/retromusic/service/CastPlayer.kt similarity index 100% rename from app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt rename to app/src/normal/java/code/name/monkey/retromusic/service/CastPlayer.kt diff --git a/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt b/app/src/normal/java/code/name/monkey/retromusic/util/AppRater.kt similarity index 99% rename from app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt rename to app/src/normal/java/code/name/monkey/retromusic/util/AppRater.kt index d6d47270e..1e3423478 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/AppRater.kt +++ b/app/src/normal/java/code/name/monkey/retromusic/util/AppRater.kt @@ -28,7 +28,6 @@ object AppRater { private const val DAYS_UNTIL_PROMPT = 3//Min number of days private const val LAUNCHES_UNTIL_PROMPT = 5//Min number of launches - @JvmStatic fun appLaunched(context: Activity) { val prefs = context.getSharedPreferences(APP_RATING, 0) if (prefs.getBoolean(DO_NOT_SHOW_AGAIN, false)) { From a1e4916ae342b29f299ef322b5ac5219a2c23721 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 15 Jun 2022 14:09:37 +0530 Subject: [PATCH 19/37] Moved ChromeCast entries of normal flavor manifest --- app/src/main/AndroidManifest.xml | 10 ---------- app/src/normal/AndroidManifest.xml | 13 ++++++++++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8eb2170a..987f89181 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,6 @@ android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions" /> - - - - - - + From 0c8ed326bfa8787e2757e825937f88167aeeaca8 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 15 Jun 2022 15:29:58 +0530 Subject: [PATCH 20/37] [ChromeCast] Use default notification when casting --- .../fragments/other/LyricsFragment.kt | 3 +-- .../retromusic/cast/CastOptionsProvider.kt | 24 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt index e2eaf6c9b..e27e31c06 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt @@ -35,7 +35,6 @@ import code.name.monkey.appthemehelper.common.ATHToolbarActivity import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper 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.activities.tageditor.TagWriter import code.name.monkey.retromusic.databinding.FragmentLyricsBinding import code.name.monkey.retromusic.databinding.FragmentNormalLyricsBinding @@ -422,7 +421,7 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { override fun onDestroyView() { super.onDestroyView() if (MusicPlayerRemote.playingQueue.isNotEmpty()) - (requireActivity() as MainActivity).expandPanel() + mainActivity.expandPanel() _binding = null } } diff --git a/app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt b/app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt index 35115dafb..6ebc71010 100644 --- a/app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt +++ b/app/src/normal/java/code/name/monkey/retromusic/cast/CastOptionsProvider.kt @@ -3,36 +3,18 @@ package code.name.monkey.retromusic.cast import android.content.Context -import code.name.monkey.retromusic.activities.MainActivity -import com.google.android.gms.cast.CastMediaControlIntent +import com.google.android.gms.cast.CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID import com.google.android.gms.cast.framework.CastOptions import com.google.android.gms.cast.framework.OptionsProvider import com.google.android.gms.cast.framework.SessionProvider import com.google.android.gms.cast.framework.media.CastMediaOptions -import com.google.android.gms.cast.framework.media.MediaIntentReceiver -import com.google.android.gms.cast.framework.media.NotificationOptions class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context): CastOptions { - val buttonActions: MutableList = ArrayList() - buttonActions.add(MediaIntentReceiver.ACTION_SKIP_PREV) - buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK) - buttonActions.add(MediaIntentReceiver.ACTION_SKIP_NEXT) - buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING) - val compatButtonActionsIndices = intArrayOf(1, 3) - val notificationOptions = NotificationOptions.Builder() - .setActions(buttonActions, compatButtonActionsIndices) - .setTargetActivityClassName(MainActivity::class.java.name) - .build() - - val mediaOptions = CastMediaOptions.Builder() - .setNotificationOptions(notificationOptions) - .setExpandedControllerActivityClassName(MainActivity::class.java.name) - .build() - + val mediaOptions = CastMediaOptions.Builder().setNotificationOptions(null).build() return CastOptions.Builder() - .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) + .setReceiverApplicationId(DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) .setCastMediaOptions(mediaOptions) .build() } From 525c5f8aa48c7adcfa53ad470cd429362f3b297c Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 15 Jun 2022 11:42:48 +0530 Subject: [PATCH 21/37] Fixed Language download --- .../retromusic/extensions/extensions.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt b/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt index 3ac58980f..59d05fb74 100644 --- a/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt +++ b/app/src/normal/java/code/name/monkey/retromusic/extensions/extensions.kt @@ -10,6 +10,8 @@ import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.play.core.splitcompat.SplitCompat import com.google.android.play.core.splitinstall.SplitInstallManagerFactory import com.google.android.play.core.splitinstall.SplitInstallRequest +import com.google.android.play.core.splitinstall.SplitInstallSessionState +import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener import java.util.* fun Context.setUpMediaRouteButton(menu: Menu) { @@ -17,7 +19,19 @@ fun Context.setUpMediaRouteButton(menu: Menu) { } fun FragmentActivity.installLanguageAndRecreate(code: String) { + var mySessionId = 0 + val manager = SplitInstallManagerFactory.create(this) + val listener = object: SplitInstallStateUpdatedListener{ + override fun onStateUpdate(state: SplitInstallSessionState) { + if (state.sessionId() == mySessionId) { + recreate() + manager.unregisterListener(this) + } + } + } + manager.registerListener(listener) + if (code != "auto") { // Try to download language resources val request = @@ -25,8 +39,11 @@ fun FragmentActivity.installLanguageAndRecreate(code: String) { .build() manager.startInstall(request) // Recreate the activity on download complete - .addOnCompleteListener { - recreate() + .addOnSuccessListener { + mySessionId = it + } + .addOnFailureListener { + showToast("Language download failed.") } } else { recreate() From d2ce889962116ebbaa988bb8e468b8e8eda90f9e Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 15 Jun 2022 17:00:54 +0530 Subject: [PATCH 22/37] Initialize BillingManager --- app/src/main/java/code/name/monkey/retromusic/App.kt | 2 ++ 1 file changed, 2 insertions(+) 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 b60870b52..705ce5ef7 100644 --- a/app/src/main/java/code/name/monkey/retromusic/App.kt +++ b/app/src/main/java/code/name/monkey/retromusic/App.kt @@ -52,6 +52,8 @@ class App : Application() { if (VersionUtils.hasNougatMR()) DynamicShortcutManager(this).initDynamicShortcuts() + billingManager = BillingManager(this) + // setting Error activity CaocConfig.Builder.create().errorActivity(ErrorActivity::class.java) .restartActivity(MainActivity::class.java).apply() From 17eb5bff05ddf591b9c25aaff73c590eb6d31b7d Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Thu, 16 Jun 2022 16:42:36 +0530 Subject: [PATCH 23/37] Update dependencies --- app/build.gradle | 3 +-- build.gradle | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 04138bfcf..75363dfcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,7 +97,7 @@ dependencies { implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.appcompat:appcompat:$appcompat_version" - implementation 'androidx.annotation:annotation:1.3.0' + implementation 'androidx.annotation:annotation:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.preference:preference-ktx:$preference_version" @@ -119,7 +119,6 @@ dependencies { implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" - def lifecycle_version = "2.5.0-rc01" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" diff --git a/build.gradle b/build.gradle index 431658a9a..539c8bf56 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,8 @@ buildscript { ext { kotlin_version = '1.7.0' - navigation_version = '2.5.0-rc01' + lifecycle_version='2.5.0-rc02' + navigation_version = '2.5.0-rc02' mdc_version = '1.7.0-alpha02' preference_version = '1.2.0' appcompat_version = '1.4.2' From 2f818ce65fe3682516c3b0f8ccf0de27d2b43899 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Thu, 16 Jun 2022 16:43:10 +0530 Subject: [PATCH 24/37] Tint NavigationRailView --- .../retromusic/billing/BillingManager.kt | 5 +- .../retromusic/extensions/ViewExtensions.kt | 12 +++++ .../monkey/retromusic/util/PreferenceUtil.kt | 4 +- ...inted.kt => TintedBottomNavigationView.kt} | 16 ++---- .../views/TintedNavigationRailView.kt | 46 ++++++++++++++++ .../sliding_music_panel_layout.xml | 2 +- .../res/layout/sliding_music_panel_layout.xml | 2 +- .../appthemehelper/util/NavigationViewUtil.kt | 52 ------------------- 8 files changed, 69 insertions(+), 70 deletions(-) rename app/src/main/java/code/name/monkey/retromusic/views/{BottomNavigationBarTinted.kt => TintedBottomNavigationView.kt} (81%) create mode 100644 app/src/main/java/code/name/monkey/retromusic/views/TintedNavigationRailView.kt delete mode 100644 appthemehelper/src/main/java/code/name/monkey/appthemehelper/util/NavigationViewUtil.kt diff --git a/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt b/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt index fa2402ef0..51c350adc 100644 --- a/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt +++ b/app/src/fdroid/java/code/name/monkey/retromusic/billing/BillingManager.kt @@ -1,6 +1,9 @@ package code.name.monkey.retromusic.billing -class BillingManager { +import android.content.Context + +@Suppress("UNUSED_PARAMETER") +class BillingManager(context: Context) { fun release() {} 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 d9ce08d9c..02764b1a0 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,9 +14,11 @@ */ package code.name.monkey.retromusic.extensions +import android.R import android.animation.Animator import android.animation.ObjectAnimator import android.animation.ValueAnimator +import android.content.res.ColorStateList import android.graphics.drawable.BitmapDrawable import android.view.LayoutInflater import android.view.View @@ -25,6 +27,7 @@ import android.view.ViewTreeObserver import android.view.animation.AnimationUtils import android.view.inputmethod.InputMethodManager import android.widget.EditText +import androidx.annotation.ColorInt import androidx.annotation.LayoutRes import androidx.annotation.Px import androidx.core.animation.doOnEnd @@ -67,6 +70,15 @@ fun EditText.appHandleColor(): EditText { return this } +fun NavigationBarView.setItemColors(@ColorInt normalColor: Int, @ColorInt selectedColor: Int) { + val csl = ColorStateList( + arrayOf(intArrayOf(-R.attr.state_checked), intArrayOf(R.attr.state_checked)), + intArrayOf(normalColor, selectedColor) + ) + itemIconTintList = csl + itemTextColor = csl +} + /** * Potentially animate showing a [BottomNavigationView]. * 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 c2fe85c2b..3881e8833 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 @@ -472,10 +472,10 @@ object PreferenceUtil { val tabTitleMode: Int get() { return when (sharedPreferences.getStringOrDefault( - TAB_TEXT_MODE, "1" + TAB_TEXT_MODE, "0" ).toInt()) { - 1 -> BottomNavigationView.LABEL_VISIBILITY_LABELED 0 -> BottomNavigationView.LABEL_VISIBILITY_AUTO + 1 -> BottomNavigationView.LABEL_VISIBILITY_LABELED 2 -> BottomNavigationView.LABEL_VISIBILITY_SELECTED 3 -> BottomNavigationView.LABEL_VISIBILITY_UNLABELED else -> BottomNavigationView.LABEL_VISIBILITY_LABELED diff --git a/app/src/main/java/code/name/monkey/retromusic/views/BottomNavigationBarTinted.kt b/app/src/main/java/code/name/monkey/retromusic/views/TintedBottomNavigationView.kt similarity index 81% rename from app/src/main/java/code/name/monkey/retromusic/views/BottomNavigationBarTinted.kt rename to app/src/main/java/code/name/monkey/retromusic/views/TintedBottomNavigationView.kt index 0e5ddb455..116f3d0a3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/views/BottomNavigationBarTinted.kt +++ b/app/src/main/java/code/name/monkey/retromusic/views/TintedBottomNavigationView.kt @@ -19,14 +19,13 @@ import android.content.res.ColorStateList import android.util.AttributeSet 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.NavigationViewUtil import code.name.monkey.retromusic.extensions.addAlpha +import code.name.monkey.retromusic.extensions.setItemColors import code.name.monkey.retromusic.util.PreferenceUtil import com.google.android.material.bottomnavigation.BottomNavigationView import dev.chrisbanes.insetter.applyInsetter -class BottomNavigationBarTinted @JvmOverloads constructor( +class TintedBottomNavigationView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, @@ -55,16 +54,7 @@ class BottomNavigationBarTinted @JvmOverloads constructor( if (!PreferenceUtil.materialYou) { val iconColor = ATHUtil.resolveColor(context, android.R.attr.colorControlNormal) val accentColor = ThemeStore.accentColor(context) - NavigationViewUtil.setItemIconColors( - this, - ColorUtil.withAlpha(iconColor, 0.5f), - accentColor - ) - NavigationViewUtil.setItemTextColors( - this, - ColorUtil.withAlpha(iconColor, 0.5f), - accentColor - ) + setItemColors(iconColor, accentColor) itemRippleColor = ColorStateList.valueOf(accentColor.addAlpha(0.08F)) itemActiveIndicatorColor = ColorStateList.valueOf(accentColor.addAlpha(0.12F)) } diff --git a/app/src/main/java/code/name/monkey/retromusic/views/TintedNavigationRailView.kt b/app/src/main/java/code/name/monkey/retromusic/views/TintedNavigationRailView.kt new file mode 100644 index 000000000..828e6fc2b --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/views/TintedNavigationRailView.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * 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.views + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import code.name.monkey.appthemehelper.util.ATHUtil +import code.name.monkey.retromusic.extensions.accentColor +import code.name.monkey.retromusic.extensions.addAlpha +import code.name.monkey.retromusic.extensions.setItemColors +import code.name.monkey.retromusic.util.PreferenceUtil +import com.google.android.material.navigationrail.NavigationRailView + +class TintedNavigationRailView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : NavigationRailView(context, attrs, defStyleAttr) { + + init { + if (!isInEditMode) { + labelVisibilityMode = PreferenceUtil.tabTitleMode + + if (!PreferenceUtil.materialYou) { + val iconColor = ATHUtil.resolveColor(context, android.R.attr.colorControlNormal) + val accentColor = context.accentColor() + setItemColors(iconColor, accentColor) + itemRippleColor = ColorStateList.valueOf(accentColor.addAlpha(0.08F)) + itemActiveIndicatorColor = ColorStateList.valueOf(accentColor.addAlpha(0.12F)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-land/sliding_music_panel_layout.xml b/app/src/main/res/layout-land/sliding_music_panel_layout.xml index c314081d8..4d6074066 100644 --- a/app/src/main/res/layout-land/sliding_music_panel_layout.xml +++ b/app/src/main/res/layout-land/sliding_music_panel_layout.xml @@ -10,7 +10,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - Date: Sat, 18 Jun 2022 11:32:33 +0530 Subject: [PATCH 25/37] Fixed queue reset when a song is clicked in Playing queue fragment --- .../java/code/name/monkey/retromusic/Constants.kt | 1 - .../retromusic/adapter/song/PlayingQueueAdapter.kt | 12 ++++++++++-- .../monkey/retromusic/helper/MusicPlayerRemote.kt | 3 --- .../name/monkey/retromusic/util/PreferenceUtil.kt | 4 +--- app/src/main/res/xml/pref_audio.xml | 8 -------- 5 files changed, 11 insertions(+), 17 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 3818186d3..d17b3194e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -136,7 +136,6 @@ const val ARTIST_DETAIL_SONG_SORT_ORDER = "artist_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" const val FILTER_SONG = "filter_song" 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 ae2de00c6..3ffad5efd 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 @@ -42,7 +42,7 @@ class PlayingQueueAdapter( activity: FragmentActivity, dataSet: MutableList, private var current: Int, - itemLayoutRes: Int + itemLayoutRes: Int, ) : SongAdapter( activity, dataSet, itemLayoutRes, null ), DraggableItemAdapter, @@ -153,6 +153,14 @@ class PlayingQueueAdapter( dragView?.isVisible = true } + override fun onClick(v: View?) { + if (isInQuickSelectMode) { + toggleChecked(layoutPosition) + } else { + MusicPlayerRemote.playSongAt(layoutPosition) + } + } + override fun onSongMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.action_remove_from_playing_queue -> { @@ -209,7 +217,7 @@ class PlayingQueueAdapter( internal class SwipedResultActionRemoveItem( private val adapter: PlayingQueueAdapter, private val position: Int, - private val activity: FragmentActivity + private val activity: FragmentActivity, ) : SwipeResultActionRemoveItem() { private var songToRemove: Song? = null 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 d3cb502da..a57af2a17 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 @@ -171,9 +171,6 @@ object MusicPlayerRemote : KoinComponent { return musicService?.playingQueue?.size ?: -1 } - /** - * Async - */ fun playSongAt(position: Int) { musicService?.playSongAt(position) } 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 3881e8833..92e01f567 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 @@ -115,7 +115,7 @@ object PreferenceUtil { putString(SAF_SDCARD_URI, value) } - val autoDownloadImagesPolicy + private val autoDownloadImagesPolicy get() = sharedPreferences.getStringOrDefault( AUTO_DOWNLOAD_IMAGES_POLICY, "only_wifi" @@ -242,8 +242,6 @@ object PreferenceUtil { val isScreenOnEnabled get() = sharedPreferences.getBoolean(KEEP_SCREEN_ON, false) - val isShuffleModeOn get() = sharedPreferences.getBoolean(TOGGLE_SHUFFLE, false) - val isSongInfo get() = sharedPreferences.getBoolean(EXTRA_SONG_INFO, false) val isPauseOnZeroVolume get() = sharedPreferences.getBoolean(PAUSE_ON_ZERO_VOLUME, false) diff --git a/app/src/main/res/xml/pref_audio.xml b/app/src/main/res/xml/pref_audio.xml index d25ba2063..749202ced 100755 --- a/app/src/main/res/xml/pref_audio.xml +++ b/app/src/main/res/xml/pref_audio.xml @@ -56,14 +56,6 @@ android:title="@string/pref_title_toggle_toggle_headset" app:icon="@drawable/ic_play_arrow" /> - - Date: Sun, 19 Jun 2022 10:38:33 +0530 Subject: [PATCH 26/37] Fixed a lyrics crash --- app/build.gradle | 4 ++-- .../fragments/player/PlayerAlbumCoverFragment.kt | 7 +++++-- build.gradle | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 75363dfcd..23217e553 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,7 +101,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.preference:preference-ktx:$preference_version" - implementation 'androidx.core:core-ktx:1.8.0' + implementation "androidx.core:core-ktx:$core_version" implementation 'androidx.palette:palette-ktx:1.0.0' implementation 'androidx.mediarouter:mediarouter:1.3.0' @@ -133,7 +133,7 @@ dependencies { def retrofit_version = '2.9.0' implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.8' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.9' def material_dialog_version = "3.3.0" implementation "com.afollestad.material-dialogs:core:$material_dialog_version" diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt index 3770da872..8cf022dca 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt @@ -52,6 +52,7 @@ import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.util.logD import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_player_album_cover), ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback, @@ -95,8 +96,10 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe if (embeddedLyrics != null) { binding.lyricsView.loadLrc(embeddedLyrics) } else { - binding.lyricsView.reset() - binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found)) + withContext(Dispatchers.Main) { + binding.lyricsView.reset() + binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found)) + } } } } diff --git a/build.gradle b/build.gradle index 539c8bf56..47d09a145 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { mdc_version = '1.7.0-alpha02' preference_version = '1.2.0' appcompat_version = '1.4.2' + core_version='1.8.0' } repositories { From e626803e9f689367fe2a2e508bae42f8f429acc6 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 19 Jun 2022 15:41:19 +0530 Subject: [PATCH 27/37] Initial fastlane structure --- README.md | 6 +++--- .../android/en-US/full_description.txt | 20 ++++++++++++++++++ .../metadata/android/en-US/images/logo.png | Bin 0 -> 13772 bytes .../en-US/images/phoneScreenshots/1.jpg | Bin .../en-US/images/phoneScreenshots/2.jpg | Bin .../en-US/images/phoneScreenshots/3.jpg | Bin .../en-US/images/phoneScreenshots/4.jpg | Bin .../en-US/images/phoneScreenshots/5.jpg | Bin .../en-US/images/phoneScreenshots/6.jpg | Bin .../en-US/images/phoneScreenshots/7.jpg | Bin .../en-US/images/phoneScreenshots/8.jpg | Bin .../android/en-US/short_description.txt | 1 + 12 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/logo.png rename screenshots/normal.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg (100%) rename screenshots/home_light.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg (100%) rename screenshots/home_dark.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg (100%) rename screenshots/home_black.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg (100%) rename screenshots/songs.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg (100%) rename screenshots/albums.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg (100%) rename screenshots/artists.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg (100%) rename screenshots/settings.jpg => fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg (100%) create mode 100644 fastlane/metadata/android/en-US/short_description.txt diff --git a/README.md b/README.md index fd6e3edb7..f745eed01 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ ___ ## 📱 Screenshots ### App Themes -| | | | +| | | | |:---:|:---:|:---:| |Clearly white| Kinda dark | Just black| ### Player screen -| | | | | | +| | | | | | |:---:|:---:|:---:|:---:|:---:| | Home | Songs | Albums | Artists | Settings | @@ -58,7 +58,7 @@ ___ | Synced Replace Cover light | Synced Replace Cover dark | Synced Replace Cover black | ### 10+ Now playing themes -| || | | | +| || | | | |:-----: |:-----: |:-----: |:-----: |:-----: | | Normal | Fit | Flat | Color | Material | diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 000000000..a39bbf5e4 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,20 @@ +Retro Music Player 🎵 + +📦 Included Features +
    +
  • Base 3 themes (Clearly White, Kinda Dark and Just Black)
  • +
  • Material You support on Android 12+
  • +
  • Gapless playback
  • +
  • Crossfade playback
  • +
  • Choose from 10+ now playing themes
  • +
  • Android auto support
  • +
  • Wallpaper accent picker on Android 8.1+
  • +
  • Home screen widgets
  • +
  • Lock screen playback controls
  • +
  • Sleep timer
  • +
  • Easy drag to sort playlist & play queue
  • +
  • Tag editor
  • +
  • Create, edit and import playlists
  • +
  • Browse and play your music by songs, albums, artists, playlists and genre
  • +
  • Smart Auto Playlists - Recently played, most played and history
  • +
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/logo.png b/fastlane/metadata/android/en-US/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..11a780e3127964864b01a4d2a3fc54aa08a4024a GIT binary patch literal 13772 zcmY*=c|6q5AMot1b)PxEjtHTgl{@UZQzU&Wq#Q{UqTD%l)uJLoN#w3{xz9wqtx8hP za_35ptRpvjJwwm;_j;bctoh8m=Y1bD@40pCs3i}lI41yr$Lf&TaR89;Clau;!avKQ z-!=izO0_aGJ`w6X`-9`2O3?kuKRWrcw`T)P-xr+-ejn9;d#6;~ZwuR`NXa+n`_VeL z&CQRgNOhcQdMGY*+VF5Q_st(_-)pVo@oi{<)6?Q*(K{dH*^P{%tp2fM`*2~{jCyo1 zA>qXriR6dL$;krDUsS!xkVMO&mk;g>3$IOJ7%fk2ASPf?RyM$DBHZy%`QU>ht8VKH z7r&Y@>5QI&67}$+fYopW0GV|kB9D|fsI72LT`DQPWK!PU^5XGuVH9yQ(ywfu0|4VG z;W|p{8xgE3L5E{=np&nH!DA94|L*Mr0CX+6PNjTsMs%I)?)V)0JV-tgvl97zEoA8J z8USC;9LH;W3;Y&-{9XRR(;!ZsuC>UMojErc2HFET!T{V~xgBp*(S5FKT3EXFx!iQy z^Mg$!h6(~G$xj!CM>6VVM{VB6f6sV~v8BJ>e;9yI!V$5HKXWmj;z)4QX8xn?JIf0V zyGnaF0m(h3xT?pK>`f1ebiJu%RU*2o1iMIcUCx9o(2CWCG*##$Kh{6HZMQM&+Df+_XMnFu;HI^n9^>t zC9#9|Hl4QAx{n#VqTU}W32$IbQs!9!i&dx1we?CqpY~xkZ3!g*POAs=)2lDCq6c>3 zqok~Gwhl+xNq#B3OC_9*G?IL%0U+6yml60#fu~}g5WpG5=_Ptq^nfT{{}sm)3#hf| z^oW3qh4GG4>@DnwN*h1328kW`3qb@6fbClT*!2d1@Upk`VgX+=hP~{dZ zZu-uXkQ<1R>cn<-ke>FqVsj-HNm7?``ws9{R&|Lu-;rP>$bqxo!ii37lMhn@Qj;j*7^=IV7Df4G3mpb8OJ0LRG-JZKo~0j>ZNQ^454PP(XS& zvHl2|5(U0b)7_hgoPh4(^l`RaXK%8+G0B%Kj{y}GKJ$9ubQt>Aq-u#&qO` z_y#tFSMfWw=LAlx!U;oS(13_$+47eU%_VFPuusl0&= z*&(34ir_EQa&rdfc2DmhwvtL%!21F@6=5?U!XzE!ri>yyW^>uRrWPR2Ax0gXKkeys!Hdq>b#d`k#$ zqQ+~!2;Y?wAKdKx0pJg(Q4B@=Q#87y!SCuz^HunNmu0K$RY9(ZCc;0d^a~%zAniA< zHC6vOWX=K74KqNctvY$=13U2IwE_L9XCeX`;`u?lK?cyY?fgOS9bp6gPuARmdCU$h zpPM-w2=J>NEUv4|vNLCWpxL#vxCSqe{XTmhfVFm(+^B1ID{5*0|Hcb>Ou8Zcny4@! zMQS3_vz^Tv;yJ(wUj{g#IIVyY!hwa0Lv`}@O=E7 zL%BU0knuWFEh4kO*}T-NgcD$eM0{>k!sA)*DV*U_NV!~cx@*_q*g z>Y-~-{>>iFLa@bsBA>#cd} z81^V#dh!)h2Rw7_>*ftr+sISv&Dl@8E4+ybe}}u@Wc2%3*g_)CPM5a@n)f@|aUS#ZLwbp{G7kGZ?mesU& zn4g!AJUX{|oTy)LhZjPhD#c)(o3X_D}nDdUZ9@fQlSmsFT!uefOS(ExaN z?&Zi+_9Ki(!d>r)R*ltUY`cTFt~XhL*gDpQdp$)1X*@{d)74sBTFHSm7~R8MnX19( zLv@42*mt~PX}zwoJg-7G^X8KLM>1tut>*YS)VtZ2p zyD^d}r`fly`034aZuk{r1F6W!LkY{uGT7 zLv8jKhkIv+xi&9;7g0a%5*UPDLIL$Fr?4lRC2GFE&GhGMVPmIv#*=v#c_}k_Eki3%(?fJ zm_O%1mJ2C5=RUkWxDcnR{r<-mwF$ybT+n?|m$Fs&zo&1F5cHe`3uGy4^M+1mZerY= z+;UI;$rhARp>kbU-TS^|s@!;j4HQ9R-^WRQiZL!l$E^PY<#1YHcmmt3)8IOcUT+rJ zF%|-`@*2Eelx!@hP3MSJm3hxR4+-=#Gsr~k9;W2e=SJ0?3=Zjz_PXWni?2S#YPG&B zE##zmC=`ykPLQS1{6=)PCK0Y3mm%)y&&{0<+>njfO){@#6Ra<}@m)+kZ-)P^#_Piu&? zIi7DZhT9+KF5~tyXR231XIBtZq3EE?e%$gZ^!UUEH_=Hvq2_{baf|}z`SC;G;754!?qq9k> zBNaF4Iv0`W;x8+Ip#ih=9o-|RI8vGU9WFE+lRA9l?Ns7kn)>rytKNy;EQ{zH1W^dd zZ15qsk~Jg#^Asyd#KWoHMfRYr0JZA21abhhv%D9AxXMFwzihr}k~XVsn(p&Q8g93% z8jZ`FNVvq}gTe~(J3;k`cB-zb6(+ahI!bv+^HFODn{@y^W>{oa1?LQR6w*R!BF<|_ z9!$Vi5f8X7%1+F_G^wQ&GViduE~Mh#t`D4Zw!bYNZD)ghi@m$hSk}n?t-h%F&W=R- zoml#^%OFTlW~dkMX%t6ffc*wO$_b=b{TYKwZnHQ|A5`uK=1}eDnY(Va3KD&1juwv+$W0>k*+Z_Yk3EoICw9LlVj8ZS} z069Vx({7JAqleYC%OgZ9Gbid_?X$RQpUtoomxw*hr}~y#j#^WZZK zyum`Q`!C}*-M#kTKAne}7FxVvh)BT&RzwQ3XCB}o-S266nvbXA-0jW1)4w--+&Ek^P5J8i2x!ea2YkGjtvMQ*9eXZ;DC)rQ1XrNoMstXT4nm(Z z*M>w~wm?*Jl=5I*ZLqkxipZCP^Ie6XkFrTC9-Lo%ai&bJ5psK>J-?{qz;14)Wz%!k z5ldpS4Rh_wn8FA%@-?jhkCgzK)NFe5+!XkV-6$>6Fz6g~2X5<_!$-A1hP2D6L6%-i zxLZ3@NhZY(@78R=>hn%+1A4basaVB~Fm4$LxcN2fNL%N7Y_J^#Knne7CtBzZ za#;s~Mj!Y#<&Mm<{`8ErzU>o9SD+VbBWTd1cMpIm7SbsR@U?pOBI>Rdf?fdn3h-`% z$STB5iXGN2FeW;YQzOhj0%{>|CVd;P)8^rzatL@uT{V7J02EkGaoi!TL%b!m2butR z0ay9R7ZlezYoA^FFlLFcW`8Ag_V}lQ=ng0eU9XOA%IPh3Uady7pNX`3r$oBT5#6T* z1d)~&(ma&fz=-07JY8-g?I5bYk(m>>EPLb*5-nF1jA%V_vu!JC;})rc*n z=Vtpd=Yt=QSMSB~{PBmA&$l3(Y}n5`x;|NOaNQCjuS03o80^TcJM~4a6Qtm!{Jr0 z$-_`?!KGgvY^%DF??fS9uMO=0UJfF?hn9itPL6xf6BoES`S9c%UPM2j5b?F4UEoX< zss$-=yI5%C(AZU&{6GtfRp}EqxS%@d>oGNTSq7xX}4wDUEN;5taM;Hz)ie`Qvb}i2i#&H{z5EM6r8re#tM4?bIvR8Bf&B6!RYIva6o9X@O*BWb6f1h5G{ z3LuGmHl#o(W^1>)FhE|yErOuggzuA$5M6QZadyTlf4}^Jg7O(K`caPEiMGC< z#cQMnjddKlcI#*T24Hjpvl%)JyowD=M(jvJEy#<=!*RYhW*~Fjk>4i}>}|{r#ha{n zkLaQas0=qygiG9z?}STBfI#2=gbnx_gDwE>3M~Dsc!k`nbK$Fs0Wk^I&@F#t|K5r& zbwOX@f^7GQ4zgx{jCdxgfNoWQa*?gBPP$La3Zt6y*SLxUn`)I~?v{wp*6innTwerE zq9|7!it)+Q(5jU*{Jd$0nq3^qT@Y8eUTlF%%@#bS$q8BXxo^}dr&$>0WYBIR(|qno z@7dsbxvDI4*;osRpA~TkGLdOPdN(H?X`18|eXdhiq_Kp`uer1haH_WiUyJSmUb>38 z-xri}ANo`Lm-VYZ^6mBk1f;Px2zJcP;{T+FIKcN3nE_d1Su_}nC9`0K*rDY98dbVS zG&DRdK7*;=Gi9= zQT;^pvk?ir^1<&9(2GjBX&RjEQN5>h&s29$vI~NJW(z#{SSYnvI}iecv2jL?77n|} zCNet=P6+*{UyVxmgQLBpBP#tF7_0*VzboGhXbuovG`3j#lq~XYF2*w>yecdpJO+JG~C?;3Ua z`Rg&$L5=~=aPQ?b(yA@$;WH@fAKQ9n*~Z=*_Q3uZC*KuC$%7O{r==baWuRda$b&aj zgc^4zwizE~_JC(UhNQeyy&AHc>GqCcB_ntin&Sldx*dL>Akr5`?qhqbS(jmcOSyw~ zqP`&r1F>D7SzQOqN#%mbi(qd|!F4)k6K}X%UpGT-lp7@Qd!TZsF^Z%_N>7`(5>RVj#c6wzJk9Wy3F9jZCktPLm#e6km~Htizf z@aqw?aQS=rOr!Q}sHPYX_{nY^-^Ovl2+W7T91t8dD+|ND?|A9U?jrd5+R z@46>#c!wV(FHe-S;%)F6B0QwKFpwm-gA?&>F+_|DjT}Nz<>-1?h`?Ud1af4!I_?QR zb@x4Tj=8jBsebOM86MJgx;ow*r6Ife2yd0PWwj?@7KJVTlpGOwHK%o|2CYdjpe8W{ zsVv_-cX@+eG{1WXNZ@EedWwtV|8zLg-&)VT%xH98vJ3Y%(Q45IN-*)X-i}(WDw?3N z;LS=^)fHQb*X#MdtE+_O4v=dgj^EM8pkKFxk*!-m8}->TXK6;Hp&A>T?P*Bgo>im+ zoMv$dNcWEC-i|VY$bdXV@1aj^8!fRX5&sTkXaVn4A`s!hHlGRl5ZHnY6FMd)j@RVF zEhs-4YAAU=MZj9`{=w_OjQdNT@&6M{#J$0CSlxvOUrQ(!Nw0p)3Y7fw>F3U%X9iI{ zA69yqjHx){HTt}#OB>^0ss-_5Z9%X*mnFMx3@iB60j&8U&e{4BcuKlcmR62%sM9x3 z0oBu+eiz*rO0!%#BBfTV`Uut8#MY@l@17<6U6byurXq$GJFu%=8)zbC9>UPmHCKMF zB&JbhLc5^E(xsr9CHC{z0J?>b$xYi`SCmgfEb9M7Kdi7LHiWfL0 z&V!9v&JlF|FO(Vn6bGuCeHOaa&o)yO4OiLwW~}_CM6~ZP=6~cY=!cED^V*6(46Kxy zmCJqy&aUFO>bDz;-=w4GaSU?>6;q@X{$=-9?4sw& zeD9dp)rsJP_0)!eHMthmno>i5;cd;xJy-2pVzle7NA%1zhWn6W`}D$F)yHq2_>8K5 z&bEzcFxa?asoAm|Cmp5o{u2nADsoA3uAgXjbq93P1olCVjqZ45zw^$%?TN z@9c%C$EAjhPtkddiv{M#<36sWmbWEwF4H>%czrKZ)^zt4KB8%bzcJzXtDCobJ)(n6 zWF}b>z7~iKJyd>bWUjb!F}b*6fiGU9UD0rkU0q?vZ|`rmo2epnhGf9Ei>tvgJGR+K zy7ziBWy{I8ARqGA?sPg9dF$xN0G;E8U{A);)mp5}k<29SWqP|Er|(e%07lE`0{ zlv&Q*?I2fE9KWYv+p(sh(>_N=1zj#Kt&^0R=$WK;MZCxgssuBfDAkYsVpEEL>M~ID z)8h30{?I9+R!cG;y+2)&2OKUyJljQYLB4z>$Um(_LNR(K1I)WWe3|VY)L_0U7``Dg zS6+rAPP<yXq zsXYWIV;?$@$kn#mY@wm|SJv(hBD!{G91(Yg>HM+lrd$e^D14_Exb)?h4h%xqdrbz< zD{V%xq!w&@7_B&u=qH`aEZE4WwjFEg|KY4>KFLz5qVXUuV&w6eBoKl z7f)B#?L;=QStXkp!o_e;8u>R$#!W8w(qEP*K)03L5wtSR8@yrStk8G&?Hmy^!*lV_ zK{w@*|K^-)<4A`&|40%tn6Wus?s>ej!r*lcu|ajwaXFRlTIT5+#XYL=A4qOUFqyL1 zEyyO0r+7L12=q)+tcQ5D)0umg?c#poU*>Job0A?mCP9 z*5}uzGr@_xrCc*~wXorA9?LenG=BOpquwcD>syf0SBE?U&^U)mfzZ#+mXn+)4h*y+ z;{U8j<0njwHDGR>6h0YuHRi3g7t7e^>6fCtPQr)tw>RfGKd?HThGtzPix8?z=MqZm=9n=ZLOGEL3vzZseQeG!{Eo!3yM z-x;E$e1w-S=B*E$C52XkPOs4pPj#VwKU0?NQ7OMWI=iXkyE>d{K`Ja=a#2mYxb4TdIy|CJDzaRwRwb~H)I@v72qs^ z1fU;T`NDjWC*ZFGdo_)kw&O2DD zTQ_cY;x97s&tV?TCV|%vp=WwH==VAw)n#yR1>|lCydKpB#Ixr9`kPDRZxnza%EoY@ z7fZ(1H$#7FR!7lI}3sYGt>xH;jD=6WfNCpyUY9EG~jmIyGFvgnqvAVCu?QQ z7KPvM1tdS+&vH#e*XjT9>uhKLqF}I;qdEXy^?|= zh%2X4Vc`^|!nN$tjgzvf5u5l!s+}r~ils?ZU6KI7sP~670q=({kLZJEgR4U-7&C`f zL-BUe5l8hlyd!@54VC{d1=LM@S-fz`G1q8qcxm^1vv5;yij8ydX zjTK^9%|Dv%FfsIDCBVc`GQqM1Ihkm*%bwfaR^=-`D(Up{N_f;L7b8Am{>{MbEc$Ck z4G&3#s)&z*Xnf2T)Mzdzu}Y%}M$;vU=aV=h{MeVPX6o7K&dcGS-x6`uS3k!CavUpa zge%=!u&8!#Bt8#r^HobUQ5s#km8xqrD#$#w!Ua7g?&=NU(~-Dd*;{D3Nu4)zSfDNq zh2TzIZn!^5D=w`fDsM3T%5nUk+JSC{H2q_rN3;<0S^@f$y~wf-YsF60Pq!nq6G~#7 zU2a7Bn(K|Vr~Z23`)Fh`J=14)e#a`3#11i5Jg7*@+gdo0s>OaWaZZF%-nSm!#7&oX z(cXN-lztoZupx%Tb0Zv_Co}*JBoDq3ra!JWg2+9LD;7CRIl;eQuXZD9xaSae1^&WJ z^xXCn-OEb&F=@|bJxt4HeW{N(&VEr^gp$LuUlZ&c?_c7Cg?5>WKUo1za``){+!YoD zrJ}$h7qK9ts>U_AGXC$?577jP$X4e4BIbENLI2_UBKpiz?v0S(bC?NEtB9n%Ea<`2-PviNn+JC4vx=zC2W$>XNDAd$9>#tJW3A-e4bv5-6s z%xhPg61Ew-TiUm#e-@Ku>Ru&H(%!)W6w}1u!FGVq^cKJL%}4kw#)e8?K(sAAFV6j1 zqo;Rd-gw*DOH5fTYUJVz6)x)xyi`sXISFS5#J(p`9~ah zB*f_gIRF`4%TQi?mU=Xe&KnwU5R_B!>$xU)7wdkl;CKWpe)lUDEJ7S4brDtk-|Hdx z=z~ojC73X$KtyV?iSslfXoLx_{o3DRs7>#5|!RWEHsw)(6$1S zM2e(VFFZ?T5lo!E(_de-5|e_d?PJ*BSxHOj0m!+v8*R zfwg?w*BtnV(A3)PYxs(ho7~$(X^#)vE=-nmth4 zXE=U`g*3vzdQsG#VsBFK1<0Sdc;5uVk{t)jGpuOizJ_$t?V{Ny^0+JSqISQJXO*cp z_^GEj0d2(vL~T`NFQG#3Gwu2dRz?%- zBX~rAjhPMK=MKl_J&F6r;8vEbFq-Qs;_dJqkaEp;-3!mzCy{1JtTKsHL)=tU;d-TGFlpJC_V+ZkruXl-GE62lI5ix5--@dCo%3G*mZIKY z#^f9`M)BJ_h&UKRP$EG-v&Au`k{XvZ!Jl7!ZOqVOgsbDog1@!8(%gy{EAE$XW}%!n>p zbup9l0%JTa$R$XOuGh=IFFpfsZF28{bWx<py&zpk`iNAnMP}t3XD^>hy z1ndB)9AJrF*yD8}-3^wFCal$@+!m zI|^H;Sn2$|%SxbWSyrRs2Nm~d9Q~@_o}keI=iEv|SFbDKXTt@aopef-xL=PCqu%CM z5C_NaVWKwB-xXQr4pRTZ{yLB`SUYYWNO@@z>IFWP8qgok2VE9|lnK-(7JVo>f*MwxkTig6Gvim%}uXqy-5!WqISc@lLx3~UOS+R-}bF;;dbbz~8!iM78zG#8e zB;+_3t+X^!UAD^0+ka+jjRgw(5yqfIjH2uMZG3qWV%uv8JP+<+@TJz53R4dY@IulQ zBwCPhTA<62bwm#%TjS$hB1mY)8y6sCJYa;=c#240*ax|VJR4BGs&AUkEv5n&X-}7v z#=^iaUXFexn$IxTs{rrCocr}8H1)OsDm#>3KdaUZb~Fruz}IC01<(Rw}Qds|g+-<~5+D&Vpf0PMnd-Nt5uae;v-_!Gm{pf-_qm6_vFfabA8wR~u1>Jkern zm~NW+z-+h2IjC(mwfvfUGoKbd_d3p&N%DI6_ab{~^Wik{db@h_L(d4Oxw@I+ zm9e%$oa?JFC#Qzw`gJYp50WiJ@{-Zg%D}IWxYwy;eAXAfcAqF=mq4L!u;Q&Veqw^& zoLP&1M|OF--UYzfIX~Kg#L|7o_`SfPzWNQhn&!3QTGP4uiuHeqs*Gv`7-{~9`6)mA zDWBdtA`RuERuVezd$k)~Gs#J$e``~OyPG>5Y1o~B3{7IiCw#u>>1Qx0z4AO~j4wg> zV<9s^gLA}i%MOSJE7}{=ZSir`xX9my>nJ(Ze(~RiZbBq)h%v_Uk$+|cl`WNLO;kAQ zHbz6{l`Zq1zaPyC+BSk5U2vl%`v+}D**Ib6%|G1m6XfK$Tuu&65A#75yrt!_e(K!Pl6p#?_pPG4uj24^~o-@o(dU6&_`p5YBb)m59wYc2Aa2 z7?UUo{cj&k(n!`D#7X93LMpPs1jTs0$&?;uU=3LW`&IVoH)ocf8y2pOVNp(40s2vuGWu$X@O%;TM#t)P$kVIrVBaCPpqb-}U zTY_t^)zzWzIpQEPRmm_hMzu#R>)|@ejk1?eM>Al1O{~VQc387rCd`H-=2Zqx$01YH zxG3p5e7s7RK%(Jv;+g2JyPY-U&@_7s;6jQWk~jBL(w(d4(e?amQOuJZ2U+x=6LY=tUa!Zkjo%h|XatE2O_E|h zO|B0+vMOax-ITyP&AZCF{?MDHT+3eimll1h?B{SLp#kxz3tj{akmA_*&ss1KXJw|$=v%JXh*~0}T(m&M*S0R}kiXKjTuq_};<`6es$GGz(!iRsH zeE{YUrO9>!YrI?9Nji8sX~L(7oWL%@qW6S|sHOZ44~WsI<{sG6Q|#T|g~E$uR^!&v z0+6%*`Rwa_F;m^AqBnWN={3f>%A`H?4@YX)5@+qUoOg#?2~&HwALBaxR<2a_-LnDo zhO3{#8~uSrv=|GNmiuC7vx>cTGHOE4j z{D``3!`bSynbLEUdKmsJbEoi;o8xg6mvE=Ol6;X7nS!K!4PX=YcA(-8)oI?{0K8>3 z_+noF$Xl)<)S-Z-ob@K2wSj!-Hm>Id#q7ov3s!RmC;B1-bMS(U)z&kkLA$P5&j%6H zUoAi#$SwO`VoRVS9=XRChySI%wU))~m*82?nH>Jn z|I`@ztv8QC{$#!W6(JM_U>_Z0M=O_qqH0F9fY&9nk>1o4GS2GsIu$ef?>b_6tB*11 zkp7-h_o!$7c-4GIxx0qq4jbgGsw7Uy#i4=m$HX_LQM9f<=0xjJ=p*0#hE0z&MV}qx zDo)nEa_zpXQK3l@4BDGtKMH>&D^LB*M>Ee#zcDphyK~Tk8YJ=IEpcR<+We*qm|X%2 zX#KAuJ^2>cf)5*_#JBcp1B`*neY*3-XVmH_J2xh0*FJr%@f8Y{a9Z%~C|p3WbHYwZ^I^5NYi8*&##XbSBV;p;P6EKUvb1!gf>3=;)7DCKmo z<$Q{)A4=vhWmyzO-^Hbjsy`nebQZy1m3WNybJr_w_E^=4pGXjy{ztlCJkH1u_6}oD zO!)e4H?ih#H?cZYY9W%X)UNLiKM5OQtIvBQWOu9ZvEUtG&+3ik{8D=kAfqIN6B1ly z2l!)G>ZD@@wKWWCJyjKB}V!qXHD|uXfUGOfcuwb zcc;`t1FYXvjoN3}aPyj|AZ+6m!hN$;e>!B22L7TSq*uD)UxZvifI-tgO_Zq*-`{)! zU@mvv{r`3b<5%ry9g+>b*F>d2uAvZ4)3#TI9rQn5bLrkFkh=C~Ma=|Y1C$tEInIQJ zcmbdhE`|$b1p9nn|JTRtx>pFdzU#Ha1Il+U@a%l^kUOGq zx9Dts!)*i*bgW!EMdE7ya8TVxj0L22QYY#g3X#sqfjy+wkt!Y_mM=${i#x{N!h*=< z?p~KOxMs(t%mQ4~Pv))5oo`0J^-;S9ZvH55utypnR$E28U6lb-k^b+74mqU z+AVw40oLJE%p@HfmaKxq2)4aI{>tb-bR?msH?s5A2LE-F3CBfJeU!f!Rn`s$&}8Sw0qO=vgYx~an+ z;Hv*+fh}V9^jn0}S9L9rP_V|+y@GmsBr16_HINlYM937To*x=A1)`nTP#2Tdk;FH-sO~VTZqyqk(bVKNx z@Wv^ve6D>m@ZW~)3gQybz;|MQe=j0VSt`*$|5Kg(As!BrPY{93 zN770u7I%=SJ&SnOn4$4f8b2fd!_qZqWEGuz`=px`1`HpntH`}<_}LMK4cnyV6LkVs zLpVXb?6f252-yJPdN1)YQIYbUPsSjnRdA#p;d+B%v=AYUl_WHwZe}{KwU9M1_;jnf zgu8Z#_jp?>aJACko11f6A5~*b@{=0vG6MJHB#v-|nE~nPpf`I}Mp5|R1WT5MIOoFn zRdH5eZQS{bpo#LgshQ$xSp%@k{=&eU=}zuk9-&C!l{rKR*Ku#ybHEc+utD@5>Frg3Z;XAzAp`#5+jxhIQH;y0ZYG-%Gzz7B;NkKdFqSD7&dy?!t_qiUhgc>sWo=`Za1OhdR^fE z_hJY>CrW$VmQx{sQoT_3oLhTp># Date: Sun, 19 Jun 2022 17:31:14 +0530 Subject: [PATCH 28/37] Changed lyrics icon --- app/src/main/res/drawable/ic_lyrics.xml | 5 ++--- app/src/main/res/drawable/ic_lyrics_outline.xml | 17 +++-------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/drawable/ic_lyrics.xml b/app/src/main/res/drawable/ic_lyrics.xml index 6a2a3a724..2064cc4a6 100644 --- a/app/src/main/res/drawable/ic_lyrics.xml +++ b/app/src/main/res/drawable/ic_lyrics.xml @@ -5,6 +5,5 @@ android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M2,22V4Q2,3.175 2.588,2.587Q3.175,2 4,2H15Q15.825,2 16.413,2.587Q17,3.175 17,4V4.425Q15.625,5.025 14.812,6.262Q14,7.5 14,9Q14,10.5 14.812,11.738Q15.625,12.975 17,13.575V16Q17,16.825 16.413,17.413Q15.825,18 15,18H6ZM6,14H10V12H6ZM19,12Q17.75,12 16.875,11.125Q16,10.25 16,9Q16,7.725 16.875,6.862Q17.75,6 19,6Q19.275,6 19.525,6.05Q19.775,6.1 20,6.175V1H24V3H22V9Q22,10.25 21.125,11.125Q20.25,12 19,12ZM6,11H13V9H6ZM6,8H13V6H6Z"/> + diff --git a/app/src/main/res/drawable/ic_lyrics_outline.xml b/app/src/main/res/drawable/ic_lyrics_outline.xml index f1d5843af..4a37173ef 100644 --- a/app/src/main/res/drawable/ic_lyrics_outline.xml +++ b/app/src/main/res/drawable/ic_lyrics_outline.xml @@ -3,18 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - + From 5ac1b2bcc67b72105c0562f90847e2069d80902c Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 19 Jun 2022 19:33:35 +0530 Subject: [PATCH 29/37] Release 6.0.2 --- app/build.gradle | 4 +-- app/src/main/assets/retro-changelog.html | 8 ++++++ .../monkey/retromusic/service/MusicService.kt | 25 +++++++++++++------ .../android/en-US/changelogs/10596.txt | 1 + 4 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/10596.txt diff --git a/app/build.gradle b/app/build.gradle index 23217e553..5dedc35c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10592 - versionName '6.0.1-beta' + versionCode 10595 + versionName '6.0.2-beta' buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"") } diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html index 18f5605c2..6028889c1 100644 --- a/app/src/main/assets/retro-changelog.html +++ b/app/src/main/assets/retro-changelog.html @@ -62,6 +62,14 @@ +
+
June 21, 2022
+

v6.0.2Beta

+

What's New

+
    +
  • Added lyrics downloading
  • +
+
June 13, 2022

v6.0.1Beta

diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt index c31293f91..087f16dd9 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt @@ -285,12 +285,16 @@ class MusicService : MediaBrowserServiceCompat(), initNotification() mediaStoreObserver = MediaStoreObserver(this, playerHandler!!) throttledSeekHandler = ThrottledSeekHandler(this, Handler(mainLooper)) - contentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + contentResolver.registerContentObserver( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, - mediaStoreObserver) - contentResolver.registerContentObserver(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, + mediaStoreObserver + ) + contentResolver.registerContentObserver( + MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, - mediaStoreObserver) + mediaStoreObserver + ) val audioVolumeObserver = AudioVolumeObserver(this) audioVolumeObserver.register(AudioManager.STREAM_MUSIC, this) registerOnSharedPreferenceChangedListener(this) @@ -318,6 +322,7 @@ class MusicService : MediaBrowserServiceCompat(), mediaSession?.isActive = false quit() releaseResources() + serviceScope.cancel() contentResolver.unregisterContentObserver(mediaStoreObserver) unregisterOnSharedPreferenceChangedListener(this) wakeLock?.release() @@ -1013,8 +1018,10 @@ class MusicService : MediaBrowserServiceCompat(), .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.albumName) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.title) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) - .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, - (getPosition() + 1).toLong()) + .putLong( + MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, + (getPosition() + 1).toLong() + ) .putLong(MediaMetadataCompat.METADATA_KEY_YEAR, song.year.toLong()) .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, null) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, playingQueue.size.toLong()) @@ -1303,8 +1310,10 @@ class MusicService : MediaBrowserServiceCompat(), } private fun setupMediaSession() { - val mediaButtonReceiverComponentName = ComponentName(applicationContext, - MediaButtonIntentReceiver::class.java) + val mediaButtonReceiverComponentName = ComponentName( + applicationContext, + MediaButtonIntentReceiver::class.java + ) val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) mediaButtonIntent.component = mediaButtonReceiverComponentName diff --git a/fastlane/metadata/android/en-US/changelogs/10596.txt b/fastlane/metadata/android/en-US/changelogs/10596.txt new file mode 100644 index 000000000..d2297558d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/10596.txt @@ -0,0 +1 @@ +Added lyrics downloading \ No newline at end of file From 0f66d005c719ab73c6454fed1a902dc39c3f9eea Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 11:28:08 +0530 Subject: [PATCH 30/37] Fixed music playing automatically after setting playback speed and pitch --- .../code/name/monkey/retromusic/service/CrossFadePlayer.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt index 7ce084f7c..3fd798351 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt @@ -333,6 +333,10 @@ internal fun crossFadeScope(): CoroutineScope = CoroutineScope(Job() + Dispatche fun MediaPlayer.setPlaybackSpeedPitch(speed: Float, pitch: Float) { if (hasMarshmallow()) { + val wasPlaying = isPlaying playbackParams = PlaybackParams().setSpeed(speed).setPitch(pitch) + if (!wasPlaying) { + pause() + } } } \ No newline at end of file From dd5945978605bc6e7f89049a2d7909d674698eb9 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 14:42:59 +0530 Subject: [PATCH 31/37] Use Coroutines in LrcView --- .../monkey/retromusic/lyrics/CoverLrcView.kt | 142 +- .../monkey/retromusic/lyrics/LrcView.java | 1235 ++++++++--------- .../name/monkey/retromusic/util/LyricUtil.kt | 8 +- .../menu/{menu_search.xml => menu_lyrics.xml} | 0 4 files changed, 574 insertions(+), 811 deletions(-) rename app/src/main/res/menu/{menu_search.xml => menu_lyrics.xml} (100%) diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt b/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt index c74c5bbdc..d772a1eb6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt @@ -19,7 +19,6 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.drawable.Drawable -import android.os.AsyncTask import android.os.Looper import android.text.Layout import android.text.StaticLayout @@ -35,14 +34,15 @@ import android.widget.Scroller import androidx.core.content.ContextCompat import androidx.core.graphics.withSave import code.name.monkey.retromusic.R +import kotlinx.coroutines.* import java.io.File +import java.lang.Runnable import kotlin.math.abs /** * 歌词 Created by wcy on 2015/11/9. */ @SuppressLint("StaticFieldLeak") -@Suppress("deprecation") class CoverLrcView @JvmOverloads constructor( context: Context?, attrs: AttributeSet? = null, @@ -72,7 +72,6 @@ class CoverLrcView @JvmOverloads constructor( private var mScroller: Scroller? = null private var mOffset = 0f private var mCurrentLine = 0 - private var flag: Any? = null private var isShowTimeline = false private var isTouching = false private var isFling = false @@ -85,9 +84,8 @@ class CoverLrcView @JvmOverloads constructor( } } - /** - * 手势监听器 - */ + private val viewScope = CoroutineScope(Dispatchers.Main + Job()) + private val mSimpleOnGestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { @@ -251,42 +249,31 @@ class CoverLrcView @JvmOverloads constructor( mScroller = Scroller(context) } - /** 设置非当前行歌词字体颜色 */ fun setNormalColor(normalColor: Int) { mNormalTextColor = normalColor postInvalidate() } - /** 设置当前行歌词的字体颜色 */ fun setCurrentColor(currentColor: Int) { mCurrentTextColor = currentColor postInvalidate() } - /** 设置拖动歌词时选中歌词的字体颜色 */ fun setTimelineTextColor(timelineTextColor: Int) { mTimelineTextColor = timelineTextColor postInvalidate() } - /** 设置拖动歌词时时间线的颜色 */ fun setTimelineColor(timelineColor: Int) { mTimelineColor = timelineColor postInvalidate() } - /** 设置拖动歌词时右侧时间字体颜色 */ fun setTimeTextColor(timeTextColor: Int) { mTimeTextColor = timeTextColor postInvalidate() } - /** - * 设置歌词是否允许拖动 - * - * @param draggable 是否允许拖动 - * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null - */ fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?) { mOnPlayClickListener = if (draggable) { requireNotNull(onPlayClickListener) { "if draggable == true, onPlayClickListener must not be null" } @@ -296,17 +283,6 @@ class CoverLrcView @JvmOverloads constructor( } } - /** - * 设置播放按钮点击监听器 - * - * @param onPlayClickListener 如果为非 null ,则激活歌词拖动功能,否则将将禁用歌词拖动功能 - */ - @Deprecated("use {@link #setDraggable(boolean, OnPlayClickListener)} instead") - fun setOnPlayClickListener(onPlayClickListener: OnPlayClickListener?) { - mOnPlayClickListener = onPlayClickListener - } - - /** 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” */ fun setLabel(label: String?) { runOnUi { mDefaultLabel = label @@ -314,106 +290,40 @@ class CoverLrcView @JvmOverloads constructor( } } - /** - * 加载歌词文件 - * - * @param lrcFile 歌词文件 - */ fun loadLrc(lrcFile: File) { - loadLrc(lrcFile, null) - } - - /** - * 加载双语歌词文件,两种语言的歌词时间戳需要一致 - * - * @param mainLrcFile 第一种语言歌词文件 - * @param secondLrcFile 第二种语言歌词文件 - */ - private fun loadLrc(mainLrcFile: File, secondLrcFile: File?) { runOnUi { reset() - val sb = StringBuilder("file://") - sb.append(mainLrcFile.path) - if (secondLrcFile != null) { - sb.append("#").append(secondLrcFile.path) + viewScope.launch(Dispatchers.IO) { + val lrcEntries = LrcUtils.parseLrc(arrayOf(lrcFile, null)) + withContext(Dispatchers.Main) { + onLrcLoaded(lrcEntries) + } } - val flag = sb.toString() - this.flag = flag - object : AsyncTask>() { - override fun doInBackground(vararg params: File?): List? { - return LrcUtils.parseLrc(params) - } - - override fun onPostExecute(lrcEntries: List) { - if (flag == flag) { - onLrcLoaded(lrcEntries) - this@CoverLrcView.flag = null - } - } - }.execute(mainLrcFile, secondLrcFile) } } - /** - * 加载歌词文本 - * - * @param lrcText 歌词文本 - */ fun loadLrc(lrcText: String?) { - loadLrc(lrcText, null) - } - - /** - * 加载双语歌词文本,两种语言的歌词时间戳需要一致 - * - * @param mainLrcText 第一种语言歌词文本 - * @param secondLrcText 第二种语言歌词文本 - */ - private fun loadLrc(mainLrcText: String?, secondLrcText: String?) { runOnUi { reset() - val sb = StringBuilder("file://") - sb.append(mainLrcText) - if (secondLrcText != null) { - sb.append("#").append(secondLrcText) + viewScope.launch(Dispatchers.IO) { + val lrcEntries = LrcUtils.parseLrc(arrayOf(lrcText, null)) + withContext(Dispatchers.Main) { + onLrcLoaded(lrcEntries) + } } - val flag = sb.toString() - this.flag = flag - object : AsyncTask>() { - override fun doInBackground(vararg params: String?): List? { - return LrcUtils.parseLrc(params) - } - - override fun onPostExecute(lrcEntries: List) { - if (flag == flag) { - onLrcLoaded(lrcEntries) - this@CoverLrcView.flag = null - } - } - }.execute(mainLrcText, secondLrcText) } } - /** - * 歌词是否有效 - * - * @return true,如果歌词有效,否则false - */ fun hasLrc(): Boolean { return mLrcEntryList.isNotEmpty() } - /** - * 刷新歌词 - * - * @param time 当前播放时间 - */ fun updateTime(time: Long) { runOnUi { if (!hasLrc()) { return@runOnUi } - val line = findShowLine(time) + val line = findShowLine(time - 300L) if (line != mCurrentLine) { mCurrentLine = line if (!isShowTimeline) { @@ -441,9 +351,9 @@ class CoverLrcView @JvmOverloads constructor( super.onDraw(canvas) val centerY = height / 2 - // 无歌词文件 if (!hasLrc()) { mLrcPaint.color = mCurrentTextColor + @Suppress("Deprecation") @SuppressLint("DrawAllocation") val staticLayout = StaticLayout( mDefaultLabel, mLrcPaint, @@ -485,11 +395,6 @@ class CoverLrcView @JvmOverloads constructor( } } - /** - * 画一行歌词 - * - * @param y 歌词中心 Y 坐标 - */ private fun drawText(canvas: Canvas, staticLayout: StaticLayout, y: Float) { canvas.withSave { translate(mLrcPadding, y - (staticLayout.height shr 1)) @@ -539,6 +444,7 @@ class CoverLrcView @JvmOverloads constructor( override fun onDetachedFromWindow() { removeCallbacks(hideTimelineRunnable) + viewScope.cancel() super.onDetachedFromWindow() } @@ -582,12 +488,10 @@ class CoverLrcView @JvmOverloads constructor( invalidate() } - /** 将中心行微调至正中心 */ private fun adjustCenter() { smoothScrollTo(centerLine, ADJUST_DURATION) } - /** 滚动到某一行 */ private fun smoothScrollTo(line: Int, duration: Long = mAnimationDuration) { val offset = getOffset(line) endAnimation() @@ -602,14 +506,12 @@ class CoverLrcView @JvmOverloads constructor( } } - /** 结束滚动动画 */ private fun endAnimation() { if (mAnimator != null && mAnimator!!.isRunning) { mAnimator!!.end() } } - /** 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) */ private fun findShowLine(time: Long): Int { var left = 0 var right = mLrcEntryList.size @@ -628,7 +530,6 @@ class CoverLrcView @JvmOverloads constructor( return 0 } - /** 获取当前在视图中央的行数 */ private val centerLine: Int get() { var centerLine = 0 @@ -642,7 +543,6 @@ class CoverLrcView @JvmOverloads constructor( return centerLine } - /** 获取歌词距离视图顶部的距离 采用懒加载方式 */ private fun getOffset(line: Int): Float { if (mLrcEntryList.isEmpty()) return 0F if (mLrcEntryList[line].offset == Float.MIN_VALUE) { @@ -656,11 +556,9 @@ class CoverLrcView @JvmOverloads constructor( return mLrcEntryList[line].offset } - /** 获取歌词宽度 */ private val lrcWidth: Float get() = width - mLrcPadding * 2 - /** 在主线程中运行 */ private fun runOnUi(r: Runnable) { if (Looper.myLooper() == Looper.getMainLooper()) { r.run() @@ -669,13 +567,7 @@ class CoverLrcView @JvmOverloads constructor( } } - /** 播放按钮点击监听器,点击后应该跳转到指定播放位置 */ fun interface OnPlayClickListener { - /** - * 播放按钮被点击,应该跳转到指定播放位置 - * - * @return 是否成功消费该事件,如果成功消费,则会更新UI - */ fun onPlayClick(time: Long): Boolean } 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 ca450b621..64844828b 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,6 +35,8 @@ import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.Scroller; +import androidx.core.content.ContextCompat; + import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -48,731 +50,600 @@ import code.name.monkey.retromusic.R; */ @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 final List mLrcEntryList = new ArrayList<>(); - private final TextPaint mLrcPaint = new TextPaint(); - private final 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 final Runnable hideTimelineRunnable = - new Runnable() { - @Override - public void run() { - if (hasLrc() && isShowTimeline) { - isShowTimeline = false; - smoothScrollTo(mCurrentLine); - } - } - }; - /** - * 手势监听器 - */ - private final 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); - } + private final List mLrcEntryList = new ArrayList<>(); + private final TextPaint mLrcPaint = new TextPaint(); + private final 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 final Runnable hideTimelineRunnable = + new Runnable() { + @Override + public void run() { + if (hasLrc() && isShowTimeline) { + isShowTimeline = false; + smoothScrollTo(mCurrentLine); + } + } + }; - @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(); - getParent().requestDisallowInterceptTouchEvent(true); - return true; - } - return super.onScroll(e1, e2, distanceX, distanceY); - } + private final 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); + } - @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); - } + @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(); + getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + return super.onScroll(e1, e2, distanceX, distanceY); + } - @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; - } - } - return super.onSingleTapConfirmed(e); - } - }; + @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); + } - public LrcView(Context context) { - this(context, null); - } + @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; + } + } + return super.onSingleTapConfirmed(e); + } + }; - 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) { + this(context, null); } - 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); - - ta.recycle(); - - 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; + public LrcView(Context context, AttributeSet attrs) { + this(context, attrs, 0); } - } - /** - * 设置播放按钮点击监听器 - * - * @param onPlayClickListener 如果为非 null ,则激活歌词拖动功能,否则将将禁用歌词拖动功能 - * @deprecated use {@link #setDraggable(boolean, OnPlayClickListener)} instead - */ - @Deprecated - public void setOnPlayClickListener(OnPlayClickListener onPlayClickListener) { - mOnPlayClickListener = onPlayClickListener; - } + public LrcView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } - /** 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” */ - 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); + 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; } - } - }.execute(lrcUrl, charset); - } - /** - * 歌词是否有效 - * - * @return true,如果歌词有效,否则false - */ - public boolean hasLrc() { - return !mLrcEntryList.isEmpty(); - } + 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) + ? ContextCompat.getDrawable(getContext(), 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); - /** - * 刷新歌词 - * - * @param time 当前播放时间 - */ - public void updateTime(long time) { - runOnUi( - () -> { - if (!hasLrc()) { - return; - } + ta.recycle(); - int line = findShowLine(time); - if (line != mCurrentLine) { - mCurrentLine = line; - if (!isShowTimeline) { - smoothScrollTo(line); - } else { - invalidate(); - } - } - }); - } + mDrawableWidth = (int) getResources().getDimension(R.dimen.lrc_drawable_width); + mTimeTextWidth = (int) getResources().getDimension(R.dimen.lrc_time_width); - /** - * 将歌词滚动到指定时间 - * - * @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.setAntiAlias(true); 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); - } - } + 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(); - /** - * 画一行歌词 - * - * @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(); + mGestureDetector = new GestureDetector(getContext(), mSimpleOnGestureListener); + mGestureDetector.setIsLongpressEnabled(false); + mScroller = new Scroller(getContext()); } - 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 setCurrentColor(int currentColor) { + mCurrentTextColor = currentColor; + postInvalidate(); } - 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 setTimelineTextColor(int timelineTextColor) { + mTimelineTextColor = timelineTextColor; + postInvalidate(); } - for (LrcEntry lrcEntry : mLrcEntryList) { - lrcEntry.init(mLrcPaint, (int) getLrcWidth(), mTextGravity); + public void setTimelineColor(int timelineColor) { + mTimelineColor = timelineColor; + postInvalidate(); } - 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(); - } + public void setTimeTextColor(int timeTextColor) { + mTimeTextColor = timeTextColor; + postInvalidate(); + } - /** 将中心行微调至正中心 */ - private void adjustCenter() { - smoothScrollTo(getCenterLine(), ADJUST_DURATION); - } - /** 滚动到某一行 */ - private void smoothScrollTo(int line) { - smoothScrollTo(line, mAnimationDuration); - } + 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(); + 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(); + public void loadLrc(File lrcFile) { + loadLrc(lrcFile, null); + } + + 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); }); - 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(); + public void loadLrc(String lrcText) { + loadLrc(lrcText, null); + } - if (time < middleTime) { - right = middle - 1; - } else { - if (middle + 1 >= mLrcEntryList.size() || time < mLrcEntryList.get(middle + 1).getTime()) { - return middle; + 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); + }); + } + + public boolean hasLrc() { + return !mLrcEntryList.isEmpty(); + } + + public void updateTime(long time) { + runOnUi( + () -> { + if (!hasLrc()) { + return; + } + + int line = findShowLine(time); + if (line != mCurrentLine) { + mCurrentLine = line; + if (!isShowTimeline) { + smoothScrollTo(line); + } else { + invalidate(); + } + } + }); + } + + @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; } - left = middle + 1; - } + 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); + } } - 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); + private void drawText(Canvas canvas, StaticLayout staticLayout, float y) { + canvas.save(); + canvas.translate(mLrcPadding, y - (staticLayout.getHeight() >> 1)); + staticLayout.draw(canvas); + canvas.restore(); } - 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); + @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); } - } - private Object getFlag() { - return mFlag; - } + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + mOffset = mScroller.getCurrY(); + invalidate(); + } - private void setFlag(Object flag) { - this.mFlag = flag; - } + if (isFling && mScroller.isFinished()) { + isFling = false; + if (hasLrc() && !isTouching) { + adjustCenter(); + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME); + } + } + } - /** 播放按钮点击监听器,点击后应该跳转到指定播放位置 */ - public interface OnPlayClickListener { - /** - * 播放按钮被点击,应该跳转到指定播放位置 - * - * @return 是否成功消费该事件,如果成功消费,则会更新UI - */ - boolean onPlayClick(long 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(); + }); + mAnimator.start(); + } + + private void endAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.end(); + } + } + + 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 { + boolean onPlayClick(long time); + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.kt b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.kt index 6db2a55c1..2f7aff94c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.kt +++ b/app/src/main/java/code/name/monkey/retromusic/util/LyricUtil.kt @@ -117,7 +117,7 @@ object LyricUtil { return "$lrcRootPath$title - $artist.lrc" } - fun getLrcOriginalPath(filePath: String): String { + private fun getLrcOriginalPath(filePath: String): String { return filePath.replace(filePath.substring(filePath.lastIndexOf(".") + 1), "lrc") } @@ -160,9 +160,9 @@ object LyricUtil { } fun getEmbeddedSyncedLyrics(data: String): String? { - val embeddedLyrics = try{ - AudioFileIO.read(File(data)).tagOrCreateDefault.getFirst(FieldKey.LYRICS) - } catch(e: Exception){ + val embeddedLyrics = try { + AudioFileIO.read(File(data)).tagOrCreateDefault.getFirst(FieldKey.LYRICS) + } catch (e: Exception) { return null } return if (AbsSynchronizedLyrics.isSynchronized(embeddedLyrics)) { diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_lyrics.xml similarity index 100% rename from app/src/main/res/menu/menu_search.xml rename to app/src/main/res/menu/menu_lyrics.xml From 62372ec205b437151db0404f9afb1a92792752f0 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 14:43:06 +0530 Subject: [PATCH 32/37] Common screen for Normal and Synced the later is preferred --- app/build.gradle | 2 +- .../base/AbsSlidingMusicPanelActivity.kt | 3 +- .../{other => lyrics}/LyricsFragment.kt | 299 +++++++----------- .../player/PlayerAlbumCoverFragment.kt | 9 +- .../playlists/PlaylistDetailsFragment.kt | 3 + .../monkey/retromusic/util/PreferenceUtil.kt | 8 +- app/src/main/res/layout/fragment_lyrics.xml | 63 ++-- .../res/layout/fragment_normal_lyrics.xml | 29 -- .../res/layout/fragment_synced_lyrics.xml | 18 -- app/src/main/res/navigation/main_graph.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 11 files changed, 177 insertions(+), 261 deletions(-) rename app/src/main/java/code/name/monkey/retromusic/fragments/{other => lyrics}/LyricsFragment.kt (58%) delete mode 100644 app/src/main/res/layout/fragment_normal_lyrics.xml delete mode 100644 app/src/main/res/layout/fragment_synced_lyrics.xml diff --git a/app/build.gradle b/app/build.gradle index 5dedc35c3..12cd27799 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ android { vectorDrawables.useSupportLibrary = true applicationId "code.name.monkey.retromusic" - versionCode 10595 + versionCode 10596 versionName '6.0.2-beta' buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"") 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 2ac89e7e8..ab31dd814 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 @@ -234,8 +234,7 @@ abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity(), navigationView.labelVisibilityMode = PreferenceUtil.tabTitleMode } TOGGLE_FULL_SCREEN -> { - if (!PreferenceUtil.isFullScreenMode) exitFullscreen() - setEdgeToEdgeOrImmersive() + recreate() } SCREEN_ON_LYRICS -> { keepScreenOn(bottomSheetBehavior.state == STATE_EXPANDED && PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics || PreferenceUtil.isScreenOnEnabled) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/lyrics/LyricsFragment.kt similarity index 58% rename from app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt rename to app/src/main/java/code/name/monkey/retromusic/fragments/lyrics/LyricsFragment.kt index e27e31c06..cd7bceb38 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/other/LyricsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/lyrics/LyricsFragment.kt @@ -12,11 +12,10 @@ * See the GNU General Public License for more details. * */ -package code.name.monkey.retromusic.fragments.other +package code.name.monkey.retromusic.fragments.lyrics import android.annotation.SuppressLint import android.app.Activity -import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.MediaStore @@ -26,22 +25,19 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController import androidx.transition.Fade -import androidx.viewpager2.adapter.FragmentStateAdapter import code.name.monkey.appthemehelper.common.ATHToolbarActivity import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.appthemehelper.util.VersionUtils import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.tageditor.TagWriter import code.name.monkey.retromusic.databinding.FragmentLyricsBinding -import code.name.monkey.retromusic.databinding.FragmentNormalLyricsBinding -import code.name.monkey.retromusic.databinding.FragmentSyncedLyricsBinding -import code.name.monkey.retromusic.extensions.* +import code.name.monkey.retromusic.extensions.accentColor +import code.name.monkey.retromusic.extensions.materialDialog +import code.name.monkey.retromusic.extensions.openUrl +import code.name.monkey.retromusic.extensions.uri import code.name.monkey.retromusic.fragments.base.AbsMainActivityFragment -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.lyrics.LrcView @@ -51,7 +47,6 @@ import code.name.monkey.retromusic.util.FileUtils import code.name.monkey.retromusic.util.LyricUtil import code.name.monkey.retromusic.util.UriUtil import com.afollestad.materialdialogs.input.input -import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jaudiotagger.audio.AudioFileIO @@ -59,14 +54,23 @@ import org.jaudiotagger.tag.FieldKey import java.io.File import java.io.FileOutputStream import java.util.* +import kotlin.collections.set -class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { +class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics), + MusicProgressViewUpdateHelper.Callback { private var _binding: FragmentLyricsBinding? = null private val binding get() = _binding!! private lateinit var song: Song - private lateinit var lyricsSectionsAdapter: LyricsSectionsAdapter + private lateinit var normalLyricsLauncher: ActivityResultLauncher + private lateinit var editSyncedLyricsLauncher: ActivityResultLauncher + + private lateinit var cacheFile: File + private var syncedLyrics: String = "" + private lateinit var syncedFileUri: Uri + + private var lyricsType: LyricsType = LyricsType.NORMAL_LYRICS private val googleSearchLrcUrl: String get() { @@ -77,13 +81,7 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { return baseUrl } - private lateinit var normalLyricsLauncher: ActivityResultLauncher - private lateinit var newSyncedLyricsLauncher: ActivityResultLauncher - private lateinit var editSyncedLyricsLauncher: ActivityResultLauncher - - private lateinit var cacheFile: File - private var syncedLyrics: String = "" - private lateinit var syncedFileUri: Uri + private lateinit var updateHelper: MusicProgressViewUpdateHelper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,14 +92,6 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { FileUtils.copyFileToUri(requireContext(), cacheFile, song.uri) } } - newSyncedLyricsLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK) { - context?.contentResolver?.openOutputStream(result.data?.data!!)?.use { - it.write(syncedLyrics.toByteArray()) - } - } - } editSyncedLyricsLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { if (it.resultCode == Activity.RESULT_OK) { @@ -118,36 +108,42 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { super.onViewCreated(view, savedInstanceState) enterTransition = Fade() exitTransition = Fade() - updateTitleSong() - lyricsSectionsAdapter = LyricsSectionsAdapter(requireActivity()) _binding = FragmentLyricsBinding.bind(view) - binding.container.transitionName = "lyrics" + updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) + updateTitleSong() + setupLyricsView() + loadLyrics() setupWakelock() setupViews() setupToolbar() } - private fun setupViews() { - binding.lyricsPager.adapter = lyricsSectionsAdapter - TabLayoutMediator(binding.tabLyrics, binding.lyricsPager) { tab, position -> - tab.text = when (position) { - 0 -> getString(R.string.synced_lyrics) - 1 -> getString(R.string.normal_lyrics) - else -> "" - } - }.attach() -// lyricsPager.isUserInputEnabled = false + private fun setupLyricsView() { + binding.lyricsView.apply { + setCurrentColor(accentColor()) + setTimeTextColor(accentColor()) + setTimelineColor(accentColor()) + setTimelineTextColor(accentColor()) + setDraggable(true, LrcView.OnPlayClickListener { + MusicPlayerRemote.seekTo(it.toInt()) + return@OnPlayClickListener true + }) + } + } - binding.tabLyrics.setSelectedTabIndicatorColor(accentColor()) - binding.tabLyrics.setTabTextColors(textColorSecondary(), accentColor()) + override fun onUpdateProgressViews(progress: Int, total: Int) { + binding.lyricsView.updateTime(progress.toLong()) + } + + private fun setupViews() { binding.editButton.accentColor() binding.editButton.setOnClickListener { - when (binding.lyricsPager.currentItem) { - 0 -> { + when (lyricsType) { + LyricsType.SYNCED_LYRICS -> { editSyncedLyrics() } - 1 -> { + LyricsType.NORMAL_LYRICS -> { editNormalLyrics() } } @@ -157,11 +153,13 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { override fun onPlayingMetaChanged() { super.onPlayingMetaChanged() updateTitleSong() + loadLyrics() } override fun onServiceConnected() { super.onServiceConnected() updateTitleSong() + loadLyrics() } private fun updateTitleSong() { @@ -181,7 +179,7 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { } override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_search, menu) + inflater.inflate(R.menu.menu_lyrics, menu) ToolbarContentTintHelper.handleOnCreateOptionsMenu( requireContext(), binding.toolbar, @@ -197,17 +195,18 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { return false } - @SuppressLint("CheckResult") - private fun editNormalLyrics() { - var content = "" - val file = File(MusicPlayerRemote.currentSong.data) - try { - content = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS) + private fun editNormalLyrics(lyrics: String? = null) { + val file = File(song.data) + val content = lyrics ?: try { + AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS) } catch (e: Exception) { e.printStackTrace() + "" } + val song = song + materialDialog().show { title(res = R.string.edit_normal_lyrics) input( @@ -217,7 +216,6 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { ) { _, input -> val fieldKeyValueMap = EnumMap(FieldKey::class.java) fieldKeyValueMap[FieldKey.LYRICS] = input.toString() - syncedLyrics = input.toString() GlobalScope.launch { if (VersionUtils.hasR()) { cacheFile = TagWriter.writeTagsToFilesR( @@ -244,7 +242,7 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { } } positiveButton(res = R.string.save) { - (lyricsSectionsAdapter.fragments[1].first as NormalLyrics).loadNormalLyrics() + loadNormalLyrics() } negativeButton(res = android.R.string.cancel) } @@ -252,9 +250,10 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { @SuppressLint("CheckResult") - private fun editSyncedLyrics() { - val content: String = LyricUtil.getStringFromLrc(LyricUtil.getSyncedLyricsFile(song)) + private fun editSyncedLyrics(lyrics: String? = null) { + val content = lyrics ?: LyricUtil.getStringFromLrc(LyricUtil.getSyncedLyricsFile(song)) + val song = song materialDialog().show { title(res = R.string.edit_synced_lyrics) input( @@ -277,145 +276,86 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { IntentSenderRequest.Builder(pendingIntent).build() ) } else { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "*/*" - intent.putExtra( - Intent.EXTRA_TITLE, - LyricUtil.getLrcOriginalPath(File(song.data).name) - ) - newSyncedLyricsLauncher.launch(intent) + val fieldKeyValueMap = EnumMap(FieldKey::class.java) + fieldKeyValueMap[FieldKey.LYRICS] = input.toString() + GlobalScope.launch { + cacheFile = TagWriter.writeTagsToFilesR( + requireContext(), + AudioTagInfo(listOf(song.data), fieldKeyValueMap, null) + )[0] + val pendingIntent = MediaStore.createWriteRequest( + requireContext().contentResolver, + listOf(song.uri) + ) + + normalLyricsLauncher.launch( + IntentSenderRequest.Builder(pendingIntent).build() + ) + } } } else { LyricUtil.writeLrc(song, input.toString()) } } positiveButton(res = R.string.save) { - (lyricsSectionsAdapter.fragments[0].first as SyncedLyrics).loadLRCLyrics() + loadLRCLyrics() } negativeButton(res = android.R.string.cancel) } } - class LyricsSectionsAdapter(fragmentActivity: FragmentActivity) : - FragmentStateAdapter(fragmentActivity) { - val fragments = listOf( - Pair(SyncedLyrics(), R.string.synced_lyrics), - Pair(NormalLyrics(), R.string.normal_lyrics) - ) - - - override fun getItemCount(): Int { - return fragments.size + private fun loadNormalLyrics() { + var lyrics: String? = null + val file = File(song.data) + try { + lyrics = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS) + } catch (e: Exception) { + e.printStackTrace() } + binding.noLyricsFound.isVisible = lyrics.isNullOrEmpty() + binding.normalLyrics.text = lyrics + } - override fun createFragment(position: Int): Fragment { - return fragments[position].first + /** + * @return success + */ + private fun loadLRCLyrics(): Boolean { + binding.lyricsView.setLabel(getString(R.string.empty)) + val lrcFile = LyricUtil.getSyncedLyricsFile(song) + if (lrcFile != null) { + binding.lyricsView.loadLrc(lrcFile) + println("File: ${lrcFile.absolutePath}") + } else { + val embeddedLyrics = LyricUtil.getEmbeddedSyncedLyrics(song.data) + if (embeddedLyrics != null) { + binding.lyricsView.loadLrc(embeddedLyrics) + println("Lyrics: ${embeddedLyrics.substring(0..50)}") + } else { + return false + } + } + return true + } + + private fun loadLyrics() { + lyricsType = if (!loadLRCLyrics()) { + loadNormalLyrics() + LyricsType.SYNCED_LYRICS + } else { + binding.noLyricsFound.isVisible = false + binding.normalLyrics.text = "" + LyricsType.NORMAL_LYRICS } } - class NormalLyrics : AbsMusicServiceFragment(R.layout.fragment_normal_lyrics) { - - private var _binding: FragmentNormalLyricsBinding? = null - private val binding get() = _binding!! - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - _binding = FragmentNormalLyricsBinding.bind(view) - loadNormalLyrics() - super.onViewCreated(view, savedInstanceState) - } - - fun loadNormalLyrics() { - var lyrics: String? = null - val file = File(MusicPlayerRemote.currentSong.data) - try { - lyrics = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS) - } catch (e: Exception) { - e.printStackTrace() - } - binding.noLyricsFound.isVisible = lyrics.isNullOrEmpty() - binding.normalLyrics.text = lyrics - } - - override fun onPlayingMetaChanged() { - super.onPlayingMetaChanged() - loadNormalLyrics() - } - - override fun onServiceConnected() { - super.onServiceConnected() - loadNormalLyrics() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } + override fun onResume() { + super.onResume() + updateHelper.start() } - class SyncedLyrics : AbsMusicServiceFragment(R.layout.fragment_synced_lyrics), - MusicProgressViewUpdateHelper.Callback { - - private var _binding: FragmentSyncedLyricsBinding? = null - private val binding get() = _binding!! - private lateinit var updateHelper: MusicProgressViewUpdateHelper - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - updateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) - _binding = FragmentSyncedLyricsBinding.bind(view) - setupLyricsView() - loadLRCLyrics() - super.onViewCreated(view, savedInstanceState) - } - - fun loadLRCLyrics() { - binding.lyricsView.setLabel(getString(R.string.empty)) - LyricUtil.getSyncedLyricsFile(MusicPlayerRemote.currentSong)?.let { - binding.lyricsView.loadLrc(it) - } - } - - private fun setupLyricsView() { - binding.lyricsView.apply { - setCurrentColor(accentColor()) - setTimeTextColor(accentColor()) - setTimelineColor(accentColor()) - setTimelineTextColor(accentColor()) - setDraggable(true, LrcView.OnPlayClickListener { - MusicPlayerRemote.seekTo(it.toInt()) - return@OnPlayClickListener true - }) - } - } - - override fun onUpdateProgressViews(progress: Int, total: Int) { - binding.lyricsView.updateTime(progress.toLong()) - } - - override fun onPlayingMetaChanged() { - super.onPlayingMetaChanged() - loadLRCLyrics() - } - - override fun onServiceConnected() { - super.onServiceConnected() - loadLRCLyrics() - } - - override fun onResume() { - super.onResume() - updateHelper.start() - } - - override fun onPause() { - super.onPause() - updateHelper.stop() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } + override fun onPause() { + super.onPause() + updateHelper.stop() } override fun onDestroyView() { @@ -424,4 +364,9 @@ class LyricsFragment : AbsMainActivityFragment(R.layout.fragment_lyrics) { mainActivity.expandPanel() _binding = null } + + enum class LyricsType { + NORMAL_LYRICS, + SYNCED_LYRICS + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt index 8cf022dca..06c15d504 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt @@ -46,7 +46,7 @@ import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.transform.CarousalPagerTransformer import code.name.monkey.retromusic.transform.ParallaxPagerTransformer import code.name.monkey.retromusic.util.LyricUtil -import code.name.monkey.retromusic.util.LyricsType +import code.name.monkey.retromusic.util.CoverLyricsType import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.util.logD @@ -174,13 +174,11 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe } override fun onServiceConnected() { - logD("Service Connected") updatePlayingQueue() updateLyrics() } override fun onPlayingMetaChanged() { - logD("Playing Meta Changed") if (viewPager.currentItem != MusicPlayerRemote.position) { viewPager.setCurrentItem(MusicPlayerRemote.position, true) } @@ -188,7 +186,6 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe } override fun onQueueChanged() { - logD("Queue Changed") updatePlayingQueue() } @@ -222,7 +219,7 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe binding.coverLyrics.isVisible = false binding.lyricsView.isVisible = false binding.viewPager.isVisible = true - val lyrics: View = if (PreferenceUtil.lyricsType == LyricsType.REPLACE_COVER) { + val lyrics: View = if (PreferenceUtil.lyricsType == CoverLyricsType.REPLACE_COVER) { ObjectAnimator.ofFloat(viewPager, View.ALPHA, if (visible) 0F else 1F).start() lrcView } else { @@ -242,7 +239,7 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe // Don't show lyrics container for below conditions if (lyricViewNpsList.contains(nps) && PreferenceUtil.showLyrics) { showLyrics(true) - if (PreferenceUtil.lyricsType == LyricsType.REPLACE_COVER) { + if (PreferenceUtil.lyricsType == CoverLyricsType.REPLACE_COVER) { progressViewUpdateHelper?.start() } } else { 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 1a71944c1..911d6f8ce 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 @@ -23,6 +23,7 @@ import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper import code.name.monkey.retromusic.interfaces.ICabCallback 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.RetroColorUtil import code.name.monkey.retromusic.util.ThemedFastScroller import com.afollestad.materialcab.attached.AttachedCab @@ -72,6 +73,8 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli binding.container.transitionName = "playlist" playlist = arguments.extraPlaylist binding.toolbar.title = playlist.playlistEntity.playlistName + binding.toolbar.subtitle = + MusicUtil.getPlaylistInfoString(requireContext(), playlist.songs.toSongs()) setUpRecyclerView() viewModel.getSongs().observe(viewLifecycleOwner) { songs(it.toSongs()) 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 92e01f567..2198d08ff 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 @@ -695,11 +695,11 @@ object PreferenceUtil { val isSnowFalling get() = sharedPreferences.getBoolean(SNOWFALL, false) - val lyricsType: LyricsType + val lyricsType: CoverLyricsType get() = if (sharedPreferences.getString(LYRICS_TYPE, "0") == "0") { - LyricsType.REPLACE_COVER + CoverLyricsType.REPLACE_COVER } else { - LyricsType.OVER_COVER + CoverLyricsType.OVER_COVER } var playbackSpeed @@ -738,6 +738,6 @@ object PreferenceUtil { get() = sharedPreferences.getBoolean(SWIPE_DOWN_DISMISS, true) } -enum class LyricsType { +enum class CoverLyricsType { REPLACE_COVER, OVER_COVER } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_lyrics.xml b/app/src/main/res/layout/fragment_lyrics.xml index 00645d23c..6b3f8ea26 100644 --- a/app/src/main/res/layout/fragment_lyrics.xml +++ b/app/src/main/res/layout/fragment_lyrics.xml @@ -28,37 +28,56 @@ android:layout_height="match_parent" android:background="?attr/colorSurface" android:gravity="start" - app:contentInsetLeft="0dp" + app:contentInsetLeft="0dp" app:contentInsetStart="0dp" app:contentInsetStartWithNavigation="0dp" app:navigationIcon="@drawable/ic_keyboard_backspace_black" - app:subtitleTextAppearance="@style/TextViewCaption" + app:title="@string/lyrics" app:titleMargin="0dp" app:titleMarginStart="0dp" - app:titleTextAppearance="@style/TextViewSubtitle1"> - - - + app:titleTextAppearance="@style/ToolbarTextAppearanceNormal" /> - + android:layout_marginTop="?attr/actionBarSize"> + + + + + + + + + + - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_synced_lyrics.xml b/app/src/main/res/layout/fragment_synced_lyrics.xml deleted file mode 100644 index 37dc17b02..000000000 --- a/app/src/main/res/layout/fragment_synced_lyrics.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - \ 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 721cab2ba..63abf5367 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -125,5 +125,5 @@ + android:name="code.name.monkey.retromusic.fragments.lyrics.LyricsFragment" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d56dbaa73..0e6eaba7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,6 +94,7 @@ Just Black Blacklist The app needs nearby devices permission to check for bluetooth devices + Nearby devices Blur Blur Card Unable to send report @@ -561,5 +562,4 @@ 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. - Nearby devices From 0c6917c775fce7d5beaaa26be137dcb3e8123504 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 15:37:57 +0530 Subject: [PATCH 33/37] Moved player toolbar for Blur Card theme to bottom --- .../player/cardblur/CardBlurFragment.kt | 12 +- .../res/layout/fragment_card_blur_player.xml | 115 ++++++++++-------- 2 files changed, 73 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt index 10db4df81..f3d26ed19 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/cardblur/CardBlurFragment.kt @@ -74,8 +74,8 @@ class CardBlurFragment : AbsPlayerFragment(R.layout.fragment_card_blur_player), libraryViewModel.updateColor(color.backgroundColor) ToolbarContentTintHelper.colorizeToolbar(binding.playerToolbar, Color.WHITE, activity) - binding.playerToolbar.setTitleTextColor(Color.WHITE) - binding.playerToolbar.setSubtitleTextColor(Color.WHITE) + binding.title.setTextColor(Color.WHITE) + binding.text.setTextColor(Color.WHITE) } override fun toggleFavorite(song: Song) { @@ -94,7 +94,7 @@ class CardBlurFragment : AbsPlayerFragment(R.layout.fragment_card_blur_player), _binding = FragmentCardBlurPlayerBinding.bind(view) setUpSubFragments() setUpPlayerToolbar() - binding.cardContainer?.drawAboveSystemBars() + binding.playerToolbar.drawAboveSystemBars() } private fun setUpSubFragments() { @@ -130,9 +130,9 @@ class CardBlurFragment : AbsPlayerFragment(R.layout.fragment_card_blur_player), private fun updateSong() { val song = MusicPlayerRemote.currentSong - binding.playerToolbar.apply { - title = song.title - subtitle = song.artistName + binding.run { + title.text = song.title + text.text = song.artistName } } diff --git a/app/src/main/res/layout/fragment_card_blur_player.xml b/app/src/main/res/layout/fragment_card_blur_player.xml index 720a35c71..4938d801b 100644 --- a/app/src/main/res/layout/fragment_card_blur_player.xml +++ b/app/src/main/res/layout/fragment_card_blur_player.xml @@ -25,64 +25,83 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + android:layout_marginTop="16dp" + android:clickable="true" + android:ellipsize="end" + android:focusable="true" + android:freezesText="true" + android:paddingHorizontal="24dp" + android:scrollHorizontally="true" + android:singleLine="true" + android:textAppearance="@style/TextViewHeadline6" + android:textColor="?android:attr/textColorPrimary" + android:textStyle="bold" + tools:text="@tools:sample/lorem" /> - + - + + + + + + + android:layout_gravity="bottom" + tools:layout="@layout/fragment_card_blur_player_playback_controls" /> + - - - - - - - - - - - + app:navigationIcon="@drawable/ic_keyboard_arrow_down_black" /> + \ No newline at end of file From c3309175a672b6b9dd09767a05adddbe0e93d0a3 Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 16:36:56 +0530 Subject: [PATCH 34/37] Colored lyrics text for Full and Gradient themes --- .../fragments/player/CoverLyricsFragment.kt | 13 +++ .../player/full/FullPlayerFragment.kt | 2 + .../player/gradient/GradientPlayerFragment.kt | 17 ++-- .../monkey/retromusic/lyrics/CoverLrcView.kt | 2 +- .../transform/ParallaxPagerTransformer.kt | 15 +--- .../layout-land/fragment_card_blur_player.xml | 81 ------------------- .../main/res/layout/fragment_cover_lyrics.xml | 4 +- app/src/main/res/layout/fragment_full.xml | 5 +- .../res/layout/fragment_gradient_player.xml | 3 +- 9 files changed, 37 insertions(+), 105 deletions(-) delete mode 100644 app/src/main/res/layout-land/fragment_card_blur_player.xml diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/CoverLyricsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/CoverLyricsFragment.kt index 7a8b8aaf6..ee32e72d3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/CoverLyricsFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/CoverLyricsFragment.kt @@ -11,6 +11,7 @@ import androidx.preference.PreferenceManager import code.name.monkey.retromusic.R import code.name.monkey.retromusic.SHOW_LYRICS import code.name.monkey.retromusic.databinding.FragmentCoverLyricsBinding +import code.name.monkey.retromusic.extensions.dipToPix import code.name.monkey.retromusic.fragments.NowPlayingScreen import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment @@ -21,6 +22,7 @@ import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.util.LyricUtil import code.name.monkey.retromusic.util.PreferenceUtil +import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jaudiotagger.audio.exceptions.CannotReadException @@ -56,6 +58,17 @@ class CoverLyricsFragment : AbsMusicServiceFragment(R.layout.fragment_cover_lyri } } + fun setColors(color: MediaNotificationProcessor) { + binding.run { + playerLyrics.background = null + playerLyricsLine1.setTextColor(color.primaryTextColor) + playerLyricsLine1.setShadowLayer(dipToPix(10f), 0f, 0f, color.backgroundColor) + playerLyricsLine2.setTextColor(color.primaryTextColor) + playerLyricsLine2.setShadowLayer(dipToPix(10f), 0f, 0f, color.backgroundColor) + } + + } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == SHOW_LYRICS) { if (sharedPreferences?.getBoolean(key, false) == true) { 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 6e0cfa836..c2ebc9428 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 @@ -28,6 +28,7 @@ import code.name.monkey.retromusic.extensions.show import code.name.monkey.retromusic.extensions.whichFragment import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.base.goToArtist +import code.name.monkey.retromusic.fragments.player.CoverLyricsFragment import code.name.monkey.retromusic.fragments.player.PlayerAlbumCoverFragment import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension @@ -98,6 +99,7 @@ class FullPlayerFragment : AbsPlayerFragment(R.layout.fragment_full) { controlsFragment.setColor(color) libraryViewModel.updateColor(color.backgroundColor) ToolbarContentTintHelper.colorizeToolbar(binding.playerToolbar, Color.WHITE, activity) + binding.coverLyrics.getFragment().setColors(color) } override fun onFavoriteToggled() { 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 e79bd784d..d85101427 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 @@ -44,6 +44,7 @@ import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.base.goToAlbum import code.name.monkey.retromusic.fragments.base.goToArtist import code.name.monkey.retromusic.fragments.other.VolumeFragment +import code.name.monkey.retromusic.fragments.player.CoverLyricsFragment import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper import code.name.monkey.retromusic.helper.PlayPauseButtonOnClickHandler @@ -270,6 +271,7 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play updateRepeatState() updateShuffleState() updatePrevNextColor() + binding.coverLyrics.getFragment().setColors(color) } override fun onFavoriteToggled() { @@ -379,17 +381,22 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play private fun setUpPlayPauseFab() { binding.playbackControlsFragment.playPauseButton.setOnClickListener( - PlayPauseButtonOnClickHandler()) + PlayPauseButtonOnClickHandler() + ) } @SuppressLint("ClickableViewAccessibility") private fun setUpPrevNext() { updatePrevNextColor() - binding.playbackControlsFragment.nextButton.setOnTouchListener(MusicSeekSkipTouchListener( - requireActivity(), - true)) + binding.playbackControlsFragment.nextButton.setOnTouchListener( + MusicSeekSkipTouchListener( + requireActivity(), + true + ) + ) binding.playbackControlsFragment.previousButton.setOnTouchListener( - MusicSeekSkipTouchListener(requireActivity(), false)) + MusicSeekSkipTouchListener(requireActivity(), false) + ) } private fun updatePrevNextColor() { diff --git a/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt b/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt index d772a1eb6..891181c3d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt +++ b/app/src/main/java/code/name/monkey/retromusic/lyrics/CoverLrcView.kt @@ -323,7 +323,7 @@ class CoverLrcView @JvmOverloads constructor( if (!hasLrc()) { return@runOnUi } - val line = findShowLine(time - 300L) + val line = findShowLine(time + 300L) if (line != mCurrentLine) { mCurrentLine = line if (!isShowTimeline) { diff --git a/app/src/main/java/code/name/monkey/retromusic/transform/ParallaxPagerTransformer.kt b/app/src/main/java/code/name/monkey/retromusic/transform/ParallaxPagerTransformer.kt index fa454dfb7..0a7d40faf 100644 --- a/app/src/main/java/code/name/monkey/retromusic/transform/ParallaxPagerTransformer.kt +++ b/app/src/main/java/code/name/monkey/retromusic/transform/ParallaxPagerTransformer.kt @@ -22,7 +22,6 @@ import androidx.viewpager.widget.ViewPager */ class ParallaxPagerTransformer(private val id: Int) : ViewPager.PageTransformer { - private var border = 0 private var speed = 0.2f override fun transformPage(page: View, position: Float) { @@ -32,23 +31,13 @@ class ParallaxPagerTransformer(private val id: Int) : ViewPager.PageTransformer if (position > -1 && position < 1) { val width = parallaxView.width.toFloat() parallaxView.translationX = -(position * width * speed) - val sc = (width - border) / width - if (position == 0f) { - scaleX = 1f - scaleY = 1f - } else { - scaleX = sc - scaleY = sc - } + scaleX = 1f + scaleY = 1f } } } } - fun setBorder(px: Int) { - border = px - } - fun setSpeed(speed: Float) { this.speed = speed } diff --git a/app/src/main/res/layout-land/fragment_card_blur_player.xml b/app/src/main/res/layout-land/fragment_card_blur_player.xml deleted file mode 100644 index d3d152cc6..000000000 --- a/app/src/main/res/layout-land/fragment_card_blur_player.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cover_lyrics.xml b/app/src/main/res/layout/fragment_cover_lyrics.xml index 50d442695..b90205a95 100644 --- a/app/src/main/res/layout/fragment_cover_lyrics.xml +++ b/app/src/main/res/layout/fragment_cover_lyrics.xml @@ -21,7 +21,7 @@ android:layout_gravity="center_vertical" android:gravity="center" android:shadowColor="@color/md_black_1000" - android:shadowRadius="4" + android:shadowRadius="10" android:textAppearance="@style/TextViewHeadline5" android:textColor="@color/md_white_1000" android:visibility="gone" @@ -34,7 +34,7 @@ android:layout_gravity="center_vertical" android:gravity="center" android:shadowColor="@color/md_black_1000" - android:shadowRadius="4" + android:shadowRadius="10" android:textAppearance="@style/TextViewHeadline5" android:textColor="@color/md_white_1000" tools:text="@tools:sample/full_names[5]" /> diff --git a/app/src/main/res/layout/fragment_full.xml b/app/src/main/res/layout/fragment_full.xml index 39fbd0acb..684bae35c 100644 --- a/app/src/main/res/layout/fragment_full.xml +++ b/app/src/main/res/layout/fragment_full.xml @@ -127,8 +127,9 @@ android:name="code.name.monkey.retromusic.fragments.player.CoverLyricsFragment" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="16dp" android:elevation="20dp" + app:layout_constraintBottom_toTopOf="@+id/playbackControlsFragment" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/playerToolbar" /> + app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_gradient_player.xml b/app/src/main/res/layout/fragment_gradient_player.xml index e4d569733..fc8e9c5f1 100644 --- a/app/src/main/res/layout/fragment_gradient_player.xml +++ b/app/src/main/res/layout/fragment_gradient_player.xml @@ -58,7 +58,8 @@ app:layout_constraintBottom_toBottomOf="@+id/playerAlbumCoverFragment" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/status_bar" /> + android:layout_marginBottom="16dp" + tools:layout="@layout/fragment_cover_lyrics" /> From 88b0299ef9c4cb6d00b19a5f3931c5ddbd554c0a Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 19:46:35 +0530 Subject: [PATCH 35/37] Update changelog --- app/src/main/assets/retro-changelog.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html index 6028889c1..da8a208f2 100644 --- a/app/src/main/assets/retro-changelog.html +++ b/app/src/main/assets/retro-changelog.html @@ -65,9 +65,9 @@
June 21, 2022

v6.0.2Beta

-

What's New

+

Fixed

    -
  • Added lyrics downloading
  • +
  • Minor bug fixes and improvements
From dba4edd8ef76c6b3eef836287520650cdf2a8e0d Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 20:08:07 +0530 Subject: [PATCH 36/37] Fixed a rare crash because of notifyDataSetChanged in Now playing ViewPager --- .../retromusic/fragments/player/PlayerAlbumCoverFragment.kt | 5 +---- app/src/main/res/layout-land/fragment_adaptive_player.xml | 4 +++- app/src/main/res/layout/fragment_adaptive_player.xml | 6 ++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt index 06c15d504..db1c641d5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt @@ -49,7 +49,6 @@ import code.name.monkey.retromusic.util.LyricUtil import code.name.monkey.retromusic.util.CoverLyricsType import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.color.MediaNotificationProcessor -import code.name.monkey.retromusic.util.logD import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -250,8 +249,7 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe private fun updatePlayingQueue() { binding.viewPager.apply { - adapter = AlbumCoverPagerAdapter(childFragmentManager, MusicPlayerRemote.playingQueue) - adapter?.notifyDataSetChanged() + adapter = AlbumCoverPagerAdapter(parentFragmentManager, MusicPlayerRemote.playingQueue) setCurrentItem(MusicPlayerRemote.position, true) onPageSelected(MusicPlayerRemote.position) } @@ -260,7 +258,6 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageSelected(position: Int) { - logD("Page Selected $position") currentPosition = position if (binding.viewPager.adapter != null) { (binding.viewPager.adapter as AlbumCoverPagerAdapter).receiveColor( diff --git a/app/src/main/res/layout-land/fragment_adaptive_player.xml b/app/src/main/res/layout-land/fragment_adaptive_player.xml index 6a193abcb..b42e7aa2f 100644 --- a/app/src/main/res/layout-land/fragment_adaptive_player.xml +++ b/app/src/main/res/layout-land/fragment_adaptive_player.xml @@ -4,7 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?colorSurface"> + android:background="?colorSurface" + android:clickable="true" + android:focusable="true"> diff --git a/app/src/main/res/layout/fragment_adaptive_player.xml b/app/src/main/res/layout/fragment_adaptive_player.xml index 3e0c27f1b..d6eca8bbe 100644 --- a/app/src/main/res/layout/fragment_adaptive_player.xml +++ b/app/src/main/res/layout/fragment_adaptive_player.xml @@ -4,7 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?colorSurface"> + android:background="?colorSurface" + android:clickable="true" + android:focusable="true"> @@ -27,7 +29,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="0" - app:contentInsetLeft="0dp" + app:contentInsetLeft="0dp" app:contentInsetStart="0dp" app:contentInsetStartWithNavigation="0dp" app:navigationIcon="@drawable/ic_keyboard_arrow_down_black" From 2edb3184b22545a7a4a6e62a7a5ab4755f397dad Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Mon, 20 Jun 2022 21:15:39 +0530 Subject: [PATCH 37/37] Update gradle wrapper jar to 7.3.3 --- gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 286 ++++++++++++++--------- gradlew.bat | 43 ++-- 4 files changed, 200 insertions(+), 133 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch literal 59536 zcma&NbC71ylI~qywr$(CZQJHswz}-9F59+k+g;UV+cs{`J?GrGXYR~=-ydruB3JCa zB64N^cILAcWk5iofq)<(fq;O7{th4@;QxID0)qN`mJ?GIqLY#rX8-|G{5M0pdVW5^ zzXk$-2kQTAC?_N@B`&6-N-rmVFE=$QD?>*=4<|!MJu@}isLc4AW#{m2if&A5T5g&~ ziuMQeS*U5sL6J698wOd)K@oK@1{peP5&Esut<#VH^u)gp`9H4)`uE!2$>RTctN+^u z=ASkePDZA-X8)rp%D;p*~P?*a_=*Kwc<^>QSH|^<0>o37lt^+Mj1;4YvJ(JR-Y+?%Nu}JAYj5 z_Qc5%Ao#F?q32i?ZaN2OSNhWL;2oDEw_({7ZbgUjna!Fqn3NzLM@-EWFPZVmc>(fZ z0&bF-Ch#p9C{YJT9Rcr3+Y_uR^At1^BxZ#eo>$PLJF3=;t_$2|t+_6gg5(j{TmjYU zK12c&lE?Eh+2u2&6Gf*IdKS&6?rYbSEKBN!rv{YCm|Rt=UlPcW9j`0o6{66#y5t9C zruFA2iKd=H%jHf%ypOkxLnO8#H}#Zt{8p!oi6)7#NqoF({t6|J^?1e*oxqng9Q2Cc zg%5Vu!em)}Yuj?kaP!D?b?(C*w!1;>R=j90+RTkyEXz+9CufZ$C^umX^+4|JYaO<5 zmIM3#dv`DGM;@F6;(t!WngZSYzHx?9&$xEF70D1BvfVj<%+b#)vz)2iLCrTeYzUcL z(OBnNoG6Le%M+@2oo)&jdOg=iCszzv59e zDRCeaX8l1hC=8LbBt|k5?CXgep=3r9BXx1uR8!p%Z|0+4Xro=xi0G!e{c4U~1j6!) zH6adq0}#l{%*1U(Cb%4AJ}VLWKBPi0MoKFaQH6x?^hQ!6em@993xdtS%_dmevzeNl z(o?YlOI=jl(`L9^ z0O+H9k$_@`6L13eTT8ci-V0ljDMD|0ifUw|Q-Hep$xYj0hTO@0%IS^TD4b4n6EKDG z??uM;MEx`s98KYN(K0>c!C3HZdZ{+_53DO%9k5W%pr6yJusQAv_;IA}925Y%;+!tY z%2k!YQmLLOr{rF~!s<3-WEUs)`ix_mSU|cNRBIWxOox_Yb7Z=~Q45ZNe*u|m^|)d* zog=i>`=bTe!|;8F+#H>EjIMcgWcG2ORD`w0WD;YZAy5#s{65~qfI6o$+Ty&-hyMyJ z3Ra~t>R!p=5ZpxA;QkDAoPi4sYOP6>LT+}{xp}tk+<0k^CKCFdNYG(Es>p0gqD)jP zWOeX5G;9(m@?GOG7g;e74i_|SmE?`B2i;sLYwRWKLy0RLW!Hx`=!LH3&k=FuCsM=9M4|GqzA)anEHfxkB z?2iK-u(DC_T1};KaUT@3nP~LEcENT^UgPvp!QC@Dw&PVAhaEYrPey{nkcn(ro|r7XUz z%#(=$7D8uP_uU-oPHhd>>^adbCSQetgSG`e$U|7mr!`|bU0aHl_cmL)na-5x1#OsVE#m*+k84Y^+UMeSAa zbrVZHU=mFwXEaGHtXQq`2ZtjfS!B2H{5A<3(nb-6ARVV8kEmOkx6D2x7~-6hl;*-*}2Xz;J#a8Wn;_B5=m zl3dY;%krf?i-Ok^Pal-}4F`{F@TYPTwTEhxpZK5WCpfD^UmM_iYPe}wpE!Djai6_{ z*pGO=WB47#Xjb7!n2Ma)s^yeR*1rTxp`Mt4sfA+`HwZf%!7ZqGosPkw69`Ix5Ku6G z@Pa;pjzV&dn{M=QDx89t?p?d9gna*}jBly*#1!6}5K<*xDPJ{wv4& zM$17DFd~L*Te3A%yD;Dp9UGWTjRxAvMu!j^Tbc}2v~q^59d4bz zvu#!IJCy(BcWTc`;v$9tH;J%oiSJ_i7s;2`JXZF+qd4C)vY!hyCtl)sJIC{ebI*0> z@x>;EzyBv>AI-~{D6l6{ST=em*U( z(r$nuXY-#CCi^8Z2#v#UXOt`dbYN1z5jzNF2 z411?w)whZrfA20;nl&C1Gi+gk<`JSm+{|*2o<< zqM#@z_D`Cn|0H^9$|Tah)0M_X4c37|KQ*PmoT@%xHc3L1ZY6(p(sNXHa&49Frzto& zR`c~ClHpE~4Z=uKa5S(-?M8EJ$zt0&fJk~p$M#fGN1-y$7!37hld`Uw>Urri(DxLa;=#rK0g4J)pXMC zxzraOVw1+kNWpi#P=6(qxf`zSdUC?D$i`8ZI@F>k6k zz21?d+dw7b&i*>Kv5L(LH-?J%@WnqT7j#qZ9B>|Zl+=> z^U-pV@1y_ptHo4hl^cPRWewbLQ#g6XYQ@EkiP z;(=SU!yhjHp%1&MsU`FV1Z_#K1&(|5n(7IHbx&gG28HNT)*~-BQi372@|->2Aw5It z0CBpUcMA*QvsPy)#lr!lIdCi@1k4V2m!NH)%Px(vu-r(Q)HYc!p zJ^$|)j^E#q#QOgcb^pd74^JUi7fUmMiNP_o*lvx*q%_odv49Dsv$NV;6J z9GOXKomA{2Pb{w}&+yHtH?IkJJu~}Z?{Uk++2mB8zyvh*xhHKE``99>y#TdD z&(MH^^JHf;g(Tbb^&8P*;_i*2&fS$7${3WJtV7K&&(MBV2~)2KB3%cWg#1!VE~k#C z!;A;?p$s{ihyojEZz+$I1)L}&G~ml=udD9qh>Tu(ylv)?YcJT3ihapi!zgPtWb*CP zlLLJSRCj-^w?@;RU9aL2zDZY1`I3d<&OMuW=c3$o0#STpv_p3b9Wtbql>w^bBi~u4 z3D8KyF?YE?=HcKk!xcp@Cigvzy=lnFgc^9c%(^F22BWYNAYRSho@~*~S)4%AhEttv zvq>7X!!EWKG?mOd9&n>vvH1p4VzE?HCuxT-u+F&mnsfDI^}*-d00-KAauEaXqg3k@ zy#)MGX!X;&3&0s}F3q40ZmVM$(H3CLfpdL?hB6nVqMxX)q=1b}o_PG%r~hZ4gUfSp zOH4qlEOW4OMUc)_m)fMR_rl^pCfXc{$fQbI*E&mV77}kRF z&{<06AJyJ!e863o-V>FA1a9Eemx6>^F$~9ppt()ZbPGfg_NdRXBWoZnDy2;#ODgf! zgl?iOcF7Meo|{AF>KDwTgYrJLb$L2%%BEtO>T$C?|9bAB&}s;gI?lY#^tttY&hfr# zKhC+&b-rpg_?~uVK%S@mQleU#_xCsvIPK*<`E0fHE1&!J7!xD#IB|SSPW6-PyuqGn3^M^Rz%WT{e?OI^svARX&SAdU77V(C~ zM$H{Kg59op{<|8ry9ecfP%=kFm(-!W&?U0@<%z*+!*<e0XesMxRFu9QnGqun6R_%T+B%&9Dtk?*d$Q zb~>84jEAPi@&F@3wAa^Lzc(AJz5gsfZ7J53;@D<;Klpl?sK&u@gie`~vTsbOE~Cd4 z%kr56mI|#b(Jk&;p6plVwmNB0H@0SmgdmjIn5Ne@)}7Vty(yb2t3ev@22AE^s!KaN zyQ>j+F3w=wnx7w@FVCRe+`vUH)3gW%_72fxzqX!S&!dchdkRiHbXW1FMrIIBwjsai8`CB2r4mAbwp%rrO>3B$Zw;9=%fXI9B{d(UzVap7u z6piC-FQ)>}VOEuPpuqznpY`hN4dGa_1Xz9rVg(;H$5Te^F0dDv*gz9JS<|>>U0J^# z6)(4ICh+N_Q`Ft0hF|3fSHs*?a=XC;e`sJaU9&d>X4l?1W=|fr!5ShD|nv$GK;j46@BV6+{oRbWfqOBRb!ir88XD*SbC(LF}I1h#6@dvK%Toe%@ zhDyG$93H8Eu&gCYddP58iF3oQH*zLbNI;rN@E{T9%A8!=v#JLxKyUe}e}BJpB{~uN zqgxRgo0*-@-iaHPV8bTOH(rS(huwK1Xg0u+e!`(Irzu@Bld&s5&bWgVc@m7;JgELd zimVs`>vQ}B_1(2#rv#N9O`fJpVfPc7V2nv34PC);Dzbb;p!6pqHzvy?2pD&1NE)?A zt(t-ucqy@wn9`^MN5apa7K|L=9>ISC>xoc#>{@e}m#YAAa1*8-RUMKwbm|;5p>T`Z zNf*ph@tnF{gmDa3uwwN(g=`Rh)4!&)^oOy@VJaK4lMT&5#YbXkl`q?<*XtsqD z9PRK6bqb)fJw0g-^a@nu`^?71k|m3RPRjt;pIkCo1{*pdqbVs-Yl>4E>3fZx3Sv44grW=*qdSoiZ9?X0wWyO4`yDHh2E!9I!ZFi zVL8|VtW38}BOJHW(Ax#KL_KQzarbuE{(%TA)AY)@tY4%A%P%SqIU~8~-Lp3qY;U-} z`h_Gel7;K1h}7$_5ZZT0&%$Lxxr-<89V&&TCsu}LL#!xpQ1O31jaa{U34~^le*Y%L za?7$>Jk^k^pS^_M&cDs}NgXlR>16AHkSK-4TRaJSh#h&p!-!vQY%f+bmn6x`4fwTp z$727L^y`~!exvmE^W&#@uY!NxJi`g!i#(++!)?iJ(1)2Wk;RN zFK&O4eTkP$Xn~4bB|q8y(btx$R#D`O@epi4ofcETrx!IM(kWNEe42Qh(8*KqfP(c0 zouBl6>Fc_zM+V;F3znbo{x#%!?mH3`_ANJ?y7ppxS@glg#S9^MXu|FM&ynpz3o&Qh z2ujAHLF3($pH}0jXQsa#?t--TnF1P73b?4`KeJ9^qK-USHE)4!IYgMn-7z|=ALF5SNGkrtPG@Y~niUQV2?g$vzJN3nZ{7;HZHzWAeQ;5P|@Tl3YHpyznGG4-f4=XflwSJY+58-+wf?~Fg@1p1wkzuu-RF3j2JX37SQUc? zQ4v%`V8z9ZVZVqS8h|@@RpD?n0W<=hk=3Cf8R?d^9YK&e9ZybFY%jdnA)PeHvtBe- zhMLD+SSteHBq*q)d6x{)s1UrsO!byyLS$58WK;sqip$Mk{l)Y(_6hEIBsIjCr5t>( z7CdKUrJTrW%qZ#1z^n*Lb8#VdfzPw~OIL76aC+Rhr<~;4Tl!sw?Rj6hXj4XWa#6Tp z@)kJ~qOV)^Rh*-?aG>ic2*NlC2M7&LUzc9RT6WM%Cpe78`iAowe!>(T0jo&ivn8-7 zs{Qa@cGy$rE-3AY0V(l8wjI^uB8Lchj@?L}fYal^>T9z;8juH@?rG&g-t+R2dVDBe zq!K%{e-rT5jX19`(bP23LUN4+_zh2KD~EAYzhpEO3MUG8@}uBHH@4J zd`>_(K4q&>*k82(dDuC)X6JuPrBBubOg7qZ{?x!r@{%0);*`h*^F|%o?&1wX?Wr4b z1~&cy#PUuES{C#xJ84!z<1tp9sfrR(i%Tu^jnXy;4`Xk;AQCdFC@?V%|; zySdC7qS|uQRcH}EFZH%mMB~7gi}a0utE}ZE_}8PQH8f;H%PN41Cb9R%w5Oi5el^fd z$n{3SqLCnrF##x?4sa^r!O$7NX!}&}V;0ZGQ&K&i%6$3C_dR%I7%gdQ;KT6YZiQrW zk%q<74oVBV>@}CvJ4Wj!d^?#Zwq(b$E1ze4$99DuNg?6t9H}k_|D7KWD7i0-g*EO7 z;5{hSIYE4DMOK3H%|f5Edx+S0VI0Yw!tsaRS2&Il2)ea^8R5TG72BrJue|f_{2UHa z@w;^c|K3da#$TB0P3;MPlF7RuQeXT$ zS<<|C0OF(k)>fr&wOB=gP8!Qm>F41u;3esv7_0l%QHt(~+n; zf!G6%hp;Gfa9L9=AceiZs~tK+Tf*Wof=4!u{nIO90jH@iS0l+#%8=~%ASzFv7zqSB^?!@N7)kp0t&tCGLmzXSRMRyxCmCYUD2!B`? zhs$4%KO~m=VFk3Buv9osha{v+mAEq=ik3RdK@;WWTV_g&-$U4IM{1IhGX{pAu%Z&H zFfwCpUsX%RKg);B@7OUzZ{Hn{q6Vv!3#8fAg!P$IEx<0vAx;GU%}0{VIsmFBPq_mb zpe^BChDK>sc-WLKl<6 zwbW|e&d&dv9Wu0goueyu>(JyPx1mz0v4E?cJjFuKF71Q1)AL8jHO$!fYT3(;U3Re* zPPOe%*O+@JYt1bW`!W_1!mN&=w3G9ru1XsmwfS~BJ))PhD(+_J_^N6j)sx5VwbWK| zwRyC?W<`pOCY)b#AS?rluxuuGf-AJ=D!M36l{ua?@SJ5>e!IBr3CXIxWw5xUZ@Xrw z_R@%?{>d%Ld4p}nEsiA@v*nc6Ah!MUs?GA7e5Q5lPpp0@`%5xY$C;{%rz24$;vR#* zBP=a{)K#CwIY%p} zXVdxTQ^HS@O&~eIftU+Qt^~(DGxrdi3k}DdT^I7Iy5SMOp$QuD8s;+93YQ!OY{eB24%xY7ml@|M7I(Nb@K_-?F;2?et|CKkuZK_>+>Lvg!>JE~wN`BI|_h6$qi!P)+K-1Hh(1;a`os z55)4Q{oJiA(lQM#;w#Ta%T0jDNXIPM_bgESMCDEg6rM33anEr}=|Fn6)|jBP6Y}u{ zv9@%7*#RI9;fv;Yii5CI+KrRdr0DKh=L>)eO4q$1zmcSmglsV`*N(x=&Wx`*v!!hn6X-l0 zP_m;X??O(skcj+oS$cIdKhfT%ABAzz3w^la-Ucw?yBPEC+=Pe_vU8nd-HV5YX6X8r zZih&j^eLU=%*;VzhUyoLF;#8QsEfmByk+Y~caBqSvQaaWf2a{JKB9B>V&r?l^rXaC z8)6AdR@Qy_BxQrE2Fk?ewD!SwLuMj@&d_n5RZFf7=>O>hzVE*seW3U?_p|R^CfoY`?|#x9)-*yjv#lo&zP=uI`M?J zbzC<^3x7GfXA4{FZ72{PE*-mNHyy59Q;kYG@BB~NhTd6pm2Oj=_ zizmD?MKVRkT^KmXuhsk?eRQllPo2Ubk=uCKiZ&u3Xjj~<(!M94c)Tez@9M1Gfs5JV z->@II)CDJOXTtPrQudNjE}Eltbjq>6KiwAwqvAKd^|g!exgLG3;wP+#mZYr`cy3#39e653d=jrR-ulW|h#ddHu(m9mFoW~2yE zz5?dB%6vF}+`-&-W8vy^OCxm3_{02royjvmwjlp+eQDzFVEUiyO#gLv%QdDSI#3W* z?3!lL8clTaNo-DVJw@ynq?q!%6hTQi35&^>P85G$TqNt78%9_sSJt2RThO|JzM$iL zg|wjxdMC2|Icc5rX*qPL(coL!u>-xxz-rFiC!6hD1IR%|HSRsV3>Kq~&vJ=s3M5y8SG%YBQ|{^l#LGlg!D?E>2yR*eV%9m$_J6VGQ~AIh&P$_aFbh zULr0Z$QE!QpkP=aAeR4ny<#3Fwyw@rZf4?Ewq`;mCVv}xaz+3ni+}a=k~P+yaWt^L z@w67!DqVf7D%7XtXX5xBW;Co|HvQ8WR1k?r2cZD%U;2$bsM%u8{JUJ5Z0k= zZJARv^vFkmWx15CB=rb=D4${+#DVqy5$C%bf`!T0+epLJLnh1jwCdb*zuCL}eEFvE z{rO1%gxg>1!W(I!owu*mJZ0@6FM(?C+d*CeceZRW_4id*D9p5nzMY&{mWqrJomjIZ z97ZNnZ3_%Hx8dn;H>p8m7F#^2;T%yZ3H;a&N7tm=Lvs&lgJLW{V1@h&6Vy~!+Ffbb zv(n3+v)_D$}dqd!2>Y2B)#<+o}LH#%ogGi2-?xRIH)1!SD)u-L65B&bsJTC=LiaF+YOCif2dUX6uAA|#+vNR z>U+KQekVGon)Yi<93(d!(yw1h3&X0N(PxN2{%vn}cnV?rYw z$N^}_o!XUB!mckL`yO1rnUaI4wrOeQ(+&k?2mi47hzxSD`N#-byqd1IhEoh!PGq>t z_MRy{5B0eKY>;Ao3z$RUU7U+i?iX^&r739F)itdrTpAi-NN0=?^m%?{A9Ly2pVv>Lqs6moTP?T2-AHqFD-o_ znVr|7OAS#AEH}h8SRPQ@NGG47dO}l=t07__+iK8nHw^(AHx&Wb<%jPc$$jl6_p(b$ z)!pi(0fQodCHfM)KMEMUR&UID>}m^(!{C^U7sBDOA)$VThRCI0_+2=( zV8mMq0R(#z;C|7$m>$>`tX+T|xGt(+Y48@ZYu#z;0pCgYgmMVbFb!$?%yhZqP_nhn zy4<#3P1oQ#2b51NU1mGnHP$cf0j-YOgAA}A$QoL6JVLcmExs(kU{4z;PBHJD%_=0F z>+sQV`mzijSIT7xn%PiDKHOujX;n|M&qr1T@rOxTdxtZ!&u&3HHFLYD5$RLQ=heur zb>+AFokUVQeJy-#LP*^)spt{mb@Mqe=A~-4p0b+Bt|pZ+@CY+%x}9f}izU5;4&QFE zO1bhg&A4uC1)Zb67kuowWY4xbo&J=%yoXlFB)&$d*-}kjBu|w!^zbD1YPc0-#XTJr z)pm2RDy%J3jlqSMq|o%xGS$bPwn4AqitC6&e?pqWcjWPt{3I{>CBy;hg0Umh#c;hU3RhCUX=8aR>rmd` z7Orw(5tcM{|-^J?ZAA9KP|)X6n9$-kvr#j5YDecTM6n z&07(nD^qb8hpF0B^z^pQ*%5ePYkv&FabrlI61ntiVp!!C8y^}|<2xgAd#FY=8b*y( zuQOuvy2`Ii^`VBNJB&R!0{hABYX55ooCAJSSevl4RPqEGb)iy_0H}v@vFwFzD%>#I>)3PsouQ+_Kkbqy*kKdHdfkN7NBcq%V{x^fSxgXpg7$bF& zj!6AQbDY(1u#1_A#1UO9AxiZaCVN2F0wGXdY*g@x$ByvUA?ePdide0dmr#}udE%K| z3*k}Vv2Ew2u1FXBaVA6aerI36R&rzEZeDDCl5!t0J=ug6kuNZzH>3i_VN`%BsaVB3 zQYw|Xub_SGf{)F{$ZX5`Jc!X!;eybjP+o$I{Z^Hsj@D=E{MnnL+TbC@HEU2DjG{3-LDGIbq()U87x4eS;JXnSh;lRlJ z>EL3D>wHt-+wTjQF$fGyDO$>d+(fq@bPpLBS~xA~R=3JPbS{tzN(u~m#Po!?H;IYv zE;?8%^vle|%#oux(Lj!YzBKv+Fd}*Ur-dCBoX*t{KeNM*n~ZPYJ4NNKkI^MFbz9!v z4(Bvm*Kc!-$%VFEewYJKz-CQN{`2}KX4*CeJEs+Q(!kI%hN1!1P6iOq?ovz}X0IOi z)YfWpwW@pK08^69#wSyCZkX9?uZD?C^@rw^Y?gLS_xmFKkooyx$*^5#cPqntNTtSG zlP>XLMj2!VF^0k#ole7`-c~*~+_T5ls?x4)ah(j8vo_ zwb%S8qoaZqY0-$ZI+ViIA_1~~rAH7K_+yFS{0rT@eQtTAdz#8E5VpwnW!zJ_^{Utv zlW5Iar3V5t&H4D6A=>?mq;G92;1cg9a2sf;gY9pJDVKn$DYdQlvfXq}zz8#LyPGq@ z+`YUMD;^-6w&r-82JL7mA8&M~Pj@aK!m{0+^v<|t%APYf7`}jGEhdYLqsHW-Le9TL z_hZZ1gbrz7$f9^fAzVIP30^KIz!!#+DRLL+qMszvI_BpOSmjtl$hh;&UeM{ER@INV zcI}VbiVTPoN|iSna@=7XkP&-4#06C};8ajbxJ4Gcq8(vWv4*&X8bM^T$mBk75Q92j z1v&%a;OSKc8EIrodmIiw$lOES2hzGDcjjB`kEDfJe{r}yE6`eZL zEB`9u>Cl0IsQ+t}`-cx}{6jqcANucqIB>Qmga_&<+80E2Q|VHHQ$YlAt{6`Qu`HA3 z03s0-sSlwbvgi&_R8s={6<~M^pGvBNjKOa>tWenzS8s zR>L7R5aZ=mSU{f?ib4Grx$AeFvtO5N|D>9#)ChH#Fny2maHWHOf2G=#<9Myot#+4u zWVa6d^Vseq_0=#AYS(-m$Lp;*8nC_6jXIjEM`omUmtH@QDs3|G)i4j*#_?#UYVZvJ z?YjT-?!4Q{BNun;dKBWLEw2C-VeAz`%?A>p;)PL}TAZn5j~HK>v1W&anteARlE+~+ zj>c(F;?qO3pXBb|#OZdQnm<4xWmn~;DR5SDMxt0UK_F^&eD|KZ=O;tO3vy4@4h^;2 zUL~-z`-P1aOe?|ZC1BgVsL)2^J-&vIFI%q@40w0{jjEfeVl)i9(~bt2z#2Vm)p`V_ z1;6$Ae7=YXk#=Qkd24Y23t&GvRxaOoad~NbJ+6pxqzJ>FY#Td7@`N5xp!n(c!=RE& z&<<@^a$_Ys8jqz4|5Nk#FY$~|FPC0`*a5HH!|Gssa9=~66&xG9)|=pOOJ2KE5|YrR zw!w6K2aC=J$t?L-;}5hn6mHd%hC;p8P|Dgh6D>hGnXPgi;6r+eA=?f72y9(Cf_ho{ zH6#)uD&R=73^$$NE;5piWX2bzR67fQ)`b=85o0eOLGI4c-Tb@-KNi2pz=Ke@SDcPn za$AxXib84`!Sf;Z3B@TSo`Dz7GM5Kf(@PR>Ghzi=BBxK8wRp>YQoXm+iL>H*Jo9M3 z6w&E?BC8AFTFT&Tv8zf+m9<&S&%dIaZ)Aoqkak_$r-2{$d~0g2oLETx9Y`eOAf14QXEQw3tJne;fdzl@wV#TFXSLXM2428F-Q}t+n2g%vPRMUzYPvzQ9f# zu(liiJem9P*?0%V@RwA7F53r~|I!Ty)<*AsMX3J{_4&}{6pT%Tpw>)^|DJ)>gpS~1rNEh z0$D?uO8mG?H;2BwM5a*26^7YO$XjUm40XmBsb63MoR;bJh63J;OngS5sSI+o2HA;W zdZV#8pDpC9Oez&L8loZO)MClRz!_!WD&QRtQxnazhT%Vj6Wl4G11nUk8*vSeVab@N#oJ}`KyJv+8Mo@T1-pqZ1t|?cnaVOd;1(h9 z!$DrN=jcGsVYE-0-n?oCJ^4x)F}E;UaD-LZUIzcD?W^ficqJWM%QLy6QikrM1aKZC zi{?;oKwq^Vsr|&`i{jIphA8S6G4)$KGvpULjH%9u(Dq247;R#l&I0{IhcC|oBF*Al zvLo7Xte=C{aIt*otJD}BUq)|_pdR>{zBMT< z(^1RpZv*l*m*OV^8>9&asGBo8h*_4q*)-eCv*|Pq=XNGrZE)^(SF7^{QE_~4VDB(o zVcPA_!G+2CAtLbl+`=Q~9iW`4ZRLku!uB?;tWqVjB0lEOf}2RD7dJ=BExy=<9wkb- z9&7{XFA%n#JsHYN8t5d~=T~5DcW4$B%3M+nNvC2`0!#@sckqlzo5;hhGi(D9=*A4` z5ynobawSPRtWn&CDLEs3Xf`(8^zDP=NdF~F^s&={l7(aw&EG}KWpMjtmz7j_VLO;@ zM2NVLDxZ@GIv7*gzl1 zjq78tv*8#WSY`}Su0&C;2F$Ze(q>F(@Wm^Gw!)(j;dk9Ad{STaxn)IV9FZhm*n+U} zi;4y*3v%A`_c7a__DJ8D1b@dl0Std3F||4Wtvi)fCcBRh!X9$1x!_VzUh>*S5s!oq z;qd{J_r79EL2wIeiGAqFstWtkfIJpjVh%zFo*=55B9Zq~y0=^iqHWfQl@O!Ak;(o*m!pZqe9 z%U2oDOhR)BvW8&F70L;2TpkzIutIvNQaTjjs5V#8mV4!NQ}zN=i`i@WI1z0eN-iCS z;vL-Wxc^Vc_qK<5RPh(}*8dLT{~GzE{w2o$2kMFaEl&q zP{V=>&3kW7tWaK-Exy{~`v4J0U#OZBk{a9{&)&QG18L@6=bsZ1zC_d{{pKZ-Ey>I> z;8H0t4bwyQqgu4hmO`3|4K{R*5>qnQ&gOfdy?z`XD%e5+pTDzUt3`k^u~SaL&XMe= z9*h#kT(*Q9jO#w2Hd|Mr-%DV8i_1{J1MU~XJ3!WUplhXDYBpJH><0OU`**nIvPIof z|N8@I=wA)sf45SAvx||f?Z5uB$kz1qL3Ky_{%RPdP5iN-D2!p5scq}buuC00C@jom zhfGKm3|f?Z0iQ|K$Z~!`8{nmAS1r+fp6r#YDOS8V*;K&Gs7Lc&f^$RC66O|)28oh`NHy&vq zJh+hAw8+ybTB0@VhWN^0iiTnLsCWbS_y`^gs!LX!Lw{yE``!UVzrV24tP8o;I6-65 z1MUiHw^{bB15tmrVT*7-#sj6cs~z`wk52YQJ*TG{SE;KTm#Hf#a~|<(|ImHH17nNM z`Ub{+J3dMD!)mzC8b(2tZtokKW5pAwHa?NFiso~# z1*iaNh4lQ4TS)|@G)H4dZV@l*Vd;Rw;-;odDhW2&lJ%m@jz+Panv7LQm~2Js6rOW3 z0_&2cW^b^MYW3)@o;neZ<{B4c#m48dAl$GCc=$>ErDe|?y@z`$uq3xd(%aAsX)D%l z>y*SQ%My`yDP*zof|3@_w#cjaW_YW4BdA;#Glg1RQcJGY*CJ9`H{@|D+*e~*457kd z73p<%fB^PV!Ybw@)Dr%(ZJbX}xmCStCYv#K3O32ej{$9IzM^I{6FJ8!(=azt7RWf4 z7ib0UOPqN40X!wOnFOoddd8`!_IN~9O)#HRTyjfc#&MCZ zZAMzOVB=;qwt8gV?{Y2?b=iSZG~RF~uyx18K)IDFLl})G1v@$(s{O4@RJ%OTJyF+Cpcx4jmy|F3euCnMK!P2WTDu5j z{{gD$=M*pH!GGzL%P)V2*ROm>!$Y=z|D`!_yY6e7SU$~a5q8?hZGgaYqaiLnkK%?0 zs#oI%;zOxF@g*@(V4p!$7dS1rOr6GVs6uYCTt2h)eB4?(&w8{#o)s#%gN@BBosRUe z)@P@8_Zm89pr~)b>e{tbPC~&_MR--iB{=)y;INU5#)@Gix-YpgP<-c2Ms{9zuCX|3 z!p(?VaXww&(w&uBHzoT%!A2=3HAP>SDxcljrego7rY|%hxy3XlODWffO_%g|l+7Y_ zqV(xbu)s4lV=l7M;f>vJl{`6qBm>#ZeMA}kXb97Z)?R97EkoI?x6Lp0yu1Z>PS?2{ z0QQ(8D)|lc9CO3B~e(pQM&5(1y&y=e>C^X$`)_&XuaI!IgDTVqt31wX#n+@!a_A0ZQkA zCJ2@M_4Gb5MfCrm5UPggeyh)8 zO9?`B0J#rkoCx(R0I!ko_2?iO@|oRf1;3r+i)w-2&j?=;NVIdPFsB)`|IC0zk6r9c zRrkfxWsiJ(#8QndNJj@{@WP2Ackr|r1VxV{7S&rSU(^)-M8gV>@UzOLXu9K<{6e{T zXJ6b92r$!|lwjhmgqkdswY&}c)KW4A)-ac%sU;2^fvq7gfUW4Bw$b!i@duy1CAxSn z(pyh$^Z=&O-q<{bZUP+$U}=*#M9uVc>CQVgDs4swy5&8RAHZ~$)hrTF4W zPsSa~qYv_0mJnF89RnnJTH`3}w4?~epFl=D(35$ zWa07ON$`OMBOHgCmfO(9RFc<)?$x)N}Jd2A(<*Ll7+4jrRt9w zwGxExUXd9VB#I|DwfxvJ;HZ8Q{37^wDhaZ%O!oO(HpcqfLH%#a#!~;Jl7F5>EX_=8 z{()l2NqPz>La3qJR;_v+wlK>GsHl;uRA8%j`A|yH@k5r%55S9{*Cp%uw6t`qc1!*T za2OeqtQj7sAp#Q~=5Fs&aCR9v>5V+s&RdNvo&H~6FJOjvaj--2sYYBvMq;55%z8^o z|BJDA4vzfow#DO#ZQHh;Oq_{r+qP{R9ox2TOgwQiv7Ow!zjN+A@BN;0tA2lUb#+zO z(^b89eV)D7UVE+h{mcNc6&GtpOqDn_?VAQ)Vob$hlFwW%xh>D#wml{t&Ofmm_d_+; zKDxzdr}`n2Rw`DtyIjrG)eD0vut$}dJAZ0AohZ+ZQdWXn_Z@dI_y=7t3q8x#pDI-K z2VVc&EGq445Rq-j0=U=Zx`oBaBjsefY;%)Co>J3v4l8V(T8H?49_@;K6q#r~Wwppc z4XW0(4k}cP=5ex>-Xt3oATZ~bBWKv)aw|I|Lx=9C1s~&b77idz({&q3T(Y(KbWO?+ zmcZ6?WeUsGk6>km*~234YC+2e6Zxdl~<_g2J|IE`GH%n<%PRv-50; zH{tnVts*S5*_RxFT9eM0z-pksIb^drUq4>QSww=u;UFCv2AhOuXE*V4z?MM`|ABOC4P;OfhS(M{1|c%QZ=!%rQTDFx`+}?Kdx$&FU?Y<$x;j7z=(;Lyz+?EE>ov!8vvMtSzG!nMie zsBa9t8as#2nH}n8xzN%W%U$#MHNXmDUVr@GX{?(=yI=4vks|V)!-W5jHsU|h_&+kY zS_8^kd3jlYqOoiI`ZqBVY!(UfnAGny!FowZWY_@YR0z!nG7m{{)4OS$q&YDyw6vC$ zm4!$h>*|!2LbMbxS+VM6&DIrL*X4DeMO!@#EzMVfr)e4Tagn~AQHIU8?e61TuhcKD zr!F4(kEebk(Wdk-?4oXM(rJwanS>Jc%<>R(siF+>+5*CqJLecP_we33iTFTXr6W^G z7M?LPC-qFHK;E!fxCP)`8rkxZyFk{EV;G-|kwf4b$c1k0atD?85+|4V%YATWMG|?K zLyLrws36p%Qz6{}>7b>)$pe>mR+=IWuGrX{3ZPZXF3plvuv5Huax86}KX*lbPVr}L z{C#lDjdDeHr~?l|)Vp_}T|%$qF&q#U;ClHEPVuS+Jg~NjC1RP=17=aQKGOcJ6B3mp z8?4*-fAD~}sX*=E6!}^u8)+m2j<&FSW%pYr_d|p_{28DZ#Cz0@NF=gC-o$MY?8Ca8 zr5Y8DSR^*urS~rhpX^05r30Ik#2>*dIOGxRm0#0YX@YQ%Mg5b6dXlS!4{7O_kdaW8PFSdj1=ryI-=5$fiieGK{LZ+SX(1b=MNL!q#lN zv98?fqqTUH8r8C7v(cx#BQ5P9W>- zmW93;eH6T`vuJ~rqtIBg%A6>q>gnWb3X!r0wh_q;211+Om&?nvYzL1hhtjB zK_7G3!n7PL>d!kj){HQE zE8(%J%dWLh1_k%gVXTZt zEdT09XSKAx27Ncaq|(vzL3gm83q>6CAw<$fTnMU05*xAe&rDfCiu`u^1)CD<>sx0i z*hr^N_TeN89G(nunZoLBf^81#pmM}>JgD@Nn1l*lN#a=B=9pN%tmvYFjFIoKe_(GF z-26x{(KXdfsQL7Uv6UtDuYwV`;8V3w>oT_I<`Ccz3QqK9tYT5ZQzbop{=I=!pMOCb zCU68`n?^DT%^&m>A%+-~#lvF!7`L7a{z<3JqIlk1$<||_J}vW1U9Y&eX<}l8##6i( zZcTT@2`9(Mecptm@{3A_Y(X`w9K0EwtPq~O!16bq{7c0f7#(3wn-^)h zxV&M~iiF!{-6A@>o;$RzQ5A50kxXYj!tcgme=Qjrbje~;5X2xryU;vH|6bE(8z^<7 zQ>BG7_c*JG8~K7Oe68i#0~C$v?-t@~@r3t2inUnLT(c=URpA9kA8uq9PKU(Ps(LVH zqgcqW>Gm?6oV#AldDPKVRcEyQIdTT`Qa1j~vS{<;SwyTdr&3*t?J)y=M7q*CzucZ&B0M=joT zBbj@*SY;o2^_h*>R0e({!QHF0=)0hOj^B^d*m>SnRrwq>MolNSgl^~r8GR#mDWGYEIJA8B<|{{j?-7p zVnV$zancW3&JVDtVpIlI|5djKq0(w$KxEFzEiiL=h5Jw~4Le23@s(mYyXWL9SX6Ot zmb)sZaly_P%BeX_9 zw&{yBef8tFm+%=--m*J|o~+Xg3N+$IH)t)=fqD+|fEk4AAZ&!wcN5=mi~Vvo^i`}> z#_3ahR}Ju)(Px7kev#JGcSwPXJ2id9%Qd2A#Uc@t8~egZ8;iC{e! z%=CGJOD1}j!HW_sgbi_8suYnn4#Ou}%9u)dXd3huFIb!ytlX>Denx@pCS-Nj$`VO&j@(z!kKSP0hE4;YIP#w9ta=3DO$7f*x zc9M4&NK%IrVmZAe=r@skWD`AEWH=g+r|*13Ss$+{c_R!b?>?UaGXlw*8qDmY#xlR= z<0XFbs2t?8i^G~m?b|!Hal^ZjRjt<@a? z%({Gn14b4-a|#uY^=@iiKH+k?~~wTj5K1A&hU z2^9-HTC)7zpoWK|$JXaBL6C z#qSNYtY>65T@Zs&-0cHeu|RX(Pxz6vTITdzJdYippF zC-EB+n4}#lM7`2Ry~SO>FxhKboIAF#Z{1wqxaCb{#yEFhLuX;Rx(Lz%T`Xo1+a2M}7D+@wol2)OJs$TwtRNJ={( zD@#zTUEE}#Fz#&(EoD|SV#bayvr&E0vzmb%H?o~46|FAcx?r4$N z&67W3mdip-T1RIxwSm_&(%U|+WvtGBj*}t69XVd&ebn>KOuL(7Y8cV?THd-(+9>G7*Nt%T zcH;`p={`SOjaf7hNd(=37Lz3-51;58JffzIPgGs_7xIOsB5p2t&@v1mKS$2D$*GQ6 zM(IR*j4{nri7NMK9xlDy-hJW6sW|ZiDRaFiayj%;(%51DN!ZCCCXz+0Vm#};70nOx zJ#yA0P3p^1DED;jGdPbQWo0WATN=&2(QybbVdhd=Vq*liDk`c7iZ?*AKEYC#SY&2g z&Q(Ci)MJ{mEat$ZdSwTjf6h~roanYh2?9j$CF@4hjj_f35kTKuGHvIs9}Re@iKMxS-OI*`0S z6s)fOtz}O$T?PLFVSeOjSO26$@u`e<>k(OSP!&YstH3ANh>)mzmKGNOwOawq-MPXe zy4xbeUAl6tamnx))-`Gi2uV5>9n(73yS)Ukma4*7fI8PaEwa)dWHs6QA6>$}7?(L8 ztN8M}?{Tf!Zu22J5?2@95&rQ|F7=FK-hihT-vDp!5JCcWrVogEnp;CHenAZ)+E+K5 z$Cffk5sNwD_?4+ymgcHR(5xgt20Z8M`2*;MzOM#>yhk{r3x=EyM226wb&!+j`W<%* zSc&|`8!>dn9D@!pYow~(DsY_naSx7(Z4i>cu#hA5=;IuI88}7f%)bRkuY2B;+9Uep zpXcvFWkJ!mQai63BgNXG26$5kyhZ2&*3Q_tk)Ii4M>@p~_~q_cE!|^A;_MHB;7s#9 zKzMzK{lIxotjc};k67^Xsl-gS!^*m*m6kn|sbdun`O?dUkJ{0cmI0-_2y=lTAfn*Y zKg*A-2sJq)CCJgY0LF-VQvl&6HIXZyxo2#!O&6fOhbHXC?%1cMc6y^*dOS{f$=137Ds1m01qs`>iUQ49JijsaQ( zksqV9@&?il$|4Ua%4!O15>Zy&%gBY&wgqB>XA3!EldQ%1CRSM(pp#k~-pkcCg4LAT zXE=puHbgsw)!xtc@P4r~Z}nTF=D2~j(6D%gTBw$(`Fc=OOQ0kiW$_RDd=hcO0t97h zb86S5r=>(@VGy1&#S$Kg_H@7G^;8Ue)X5Y+IWUi`o;mpvoV)`fcVk4FpcT|;EG!;? zHG^zrVVZOm>1KFaHlaogcWj(v!S)O(Aa|Vo?S|P z5|6b{qkH(USa*Z7-y_Uvty_Z1|B{rTS^qmEMLEYUSk03_Fg&!O3BMo{b^*`3SHvl0 zhnLTe^_vVIdcSHe)SQE}r~2dq)VZJ!aSKR?RS<(9lzkYo&dQ?mubnWmgMM37Nudwo z3Vz@R{=m2gENUE3V4NbIzAA$H1z0pagz94-PTJyX{b$yndsdKptmlKQKaaHj@3=ED zc7L?p@%ui|RegVYutK$64q4pe9+5sv34QUpo)u{1ci?)_7gXQd{PL>b0l(LI#rJmN zGuO+%GO`xneFOOr4EU(Wg}_%bhzUf;d@TU+V*2#}!2OLwg~%D;1FAu=Un>OgjPb3S z7l(riiCwgghC=Lm5hWGf5NdGp#01xQ59`HJcLXbUR3&n%P(+W2q$h2Qd z*6+-QXJ*&Kvk9ht0f0*rO_|FMBALen{j7T1l%=Q>gf#kma zQlg#I9+HB+z*5BMxdesMND`_W;q5|FaEURFk|~&{@qY32N$G$2B=&Po{=!)x5b!#n zxLzblkq{yj05#O7(GRuT39(06FJlalyv<#K4m}+vs>9@q-&31@1(QBv82{}Zkns~K ze{eHC_RDX0#^A*JQTwF`a=IkE6Ze@j#-8Q`tTT?k9`^ZhA~3eCZJ-Jr{~7Cx;H4A3 zcZ+Zj{mzFZbVvQ6U~n>$U2ZotGsERZ@}VKrgGh0xM;Jzt29%TX6_&CWzg+YYMozrM z`nutuS)_0dCM8UVaKRj804J4i%z2BA_8A4OJRQ$N(P9Mfn-gF;4#q788C@9XR0O3< zsoS4wIoyt046d+LnSCJOy@B@Uz*#GGd#+Ln1ek5Dv>(ZtD@tgZlPnZZJGBLr^JK+!$$?A_fA3LOrkoDRH&l7 zcMcD$Hsjko3`-{bn)jPL6E9Ds{WskMrivsUu5apD z?grQO@W7i5+%X&E&p|RBaEZ(sGLR@~(y^BI@lDMot^Ll?!`90KT!JXUhYS`ZgX3jnu@Ja^seA*M5R@f`=`ynQV4rc$uT1mvE?@tz)TN<=&H1%Z?5yjxcpO+6y_R z6EPuPKM5uxKpmZfT(WKjRRNHs@ib)F5WAP7QCADvmCSD#hPz$V10wiD&{NXyEwx5S z6NE`3z!IS^$s7m}PCwQutVQ#~w+V z=+~->DI*bR2j0^@dMr9`p>q^Ny~NrAVxrJtX2DUveic5vM%#N*XO|?YAWwNI$Q)_) zvE|L(L1jP@F%gOGtnlXtIv2&1i8q<)Xfz8O3G^Ea~e*HJsQgBxWL(yuLY+jqUK zRE~`-zklrGog(X}$9@ZVUw!8*=l`6mzYLtsg`AvBYz(cxmAhr^j0~(rzXdiOEeu_p zE$sf2(w(BPAvO5DlaN&uQ$4@p-b?fRs}d7&2UQ4Fh?1Hzu*YVjcndqJLw0#q@fR4u zJCJ}>_7-|QbvOfylj+e^_L`5Ep9gqd>XI3-O?Wp z-gt*P29f$Tx(mtS`0d05nHH=gm~Po_^OxxUwV294BDKT>PHVlC5bndncxGR!n(OOm znsNt@Q&N{TLrmsoKFw0&_M9$&+C24`sIXGWgQaz=kY;S{?w`z^Q0JXXBKFLj0w0U6P*+jPKyZHX9F#b0D1$&(- zrm8PJd?+SrVf^JlfTM^qGDK&-p2Kdfg?f>^%>1n8bu&byH(huaocL>l@f%c*QkX2i znl}VZ4R1en4S&Bcqw?$=Zi7ohqB$Jw9x`aM#>pHc0x z0$!q7iFu zZ`tryM70qBI6JWWTF9EjgG@>6SRzsd}3h+4D8d~@CR07P$LJ}MFsYi-*O%XVvD@yT|rJ+Mk zDllJ7$n0V&A!0flbOf)HE6P_afPWZmbhpliqJuw=-h+r;WGk|ntkWN(8tKlYpq5Ow z(@%s>IN8nHRaYb*^d;M(D$zGCv5C|uqmsDjwy4g=Lz>*OhO3z=)VD}C<65;`89Ye} zSCxrv#ILzIpEx1KdLPlM&%Cctf@FqTKvNPXC&`*H9=l=D3r!GLM?UV zOxa(8ZsB`&+76S-_xuj?G#wXBfDY@Z_tMpXJS7^mp z@YX&u0jYw2A+Z+bD#6sgVK5ZgdPSJV3>{K^4~%HV?rn~4D)*2H!67Y>0aOmzup`{D zzDp3c9yEbGCY$U<8biJ_gB*`jluz1ShUd!QUIQJ$*1;MXCMApJ^m*Fiv88RZ zFopLViw}{$Tyhh_{MLGIE2~sZ)t0VvoW%=8qKZ>h=adTe3QM$&$PO2lfqH@brt!9j ziePM8$!CgE9iz6B<6_wyTQj?qYa;eC^{x_0wuwV~W+^fZmFco-o%wsKSnjXFEx02V zF5C2t)T6Gw$Kf^_c;Ei3G~uC8SM-xyycmXyC2hAVi-IfXqhu$$-C=*|X?R0~hu z8`J6TdgflslhrmDZq1f?GXF7*ALeMmOEpRDg(s*H`4>_NAr`2uqF;k;JQ+8>A|_6ZNsNLECC%NNEb1Y1dP zbIEmNpK)#XagtL4R6BC{C5T(+=yA-(Z|Ap}U-AfZM#gwVpus3(gPn}Q$CExObJ5AC z)ff9Yk?wZ}dZ-^)?cbb9Fw#EjqQ8jxF4G3=L?Ra zg_)0QDMV1y^A^>HRI$x?Op@t;oj&H@1xt4SZ9(kifQ zb59B*`M99Td7@aZ3UWvj1rD0sE)d=BsBuW*KwkCds7ay(7*01_+L}b~7)VHI>F_!{ zyxg-&nCO?v#KOUec0{OOKy+sjWA;8rTE|Lv6I9H?CI?H(mUm8VXGwU$49LGpz&{nQp2}dinE1@lZ1iox6{ghN&v^GZv9J${7WaXj)<0S4g_uiJ&JCZ zr8-hsu`U%N;+9N^@&Q0^kVPB3)wY(rr}p7{p0qFHb3NUUHJb672+wRZs`gd1UjKPX z4o6zljKKA+Kkj?H>Ew63o%QjyBk&1!P22;MkD>sM0=z_s-G{mTixJCT9@_|*(p^bz zJ8?ZZ&;pzV+7#6Mn`_U-)k8Pjg?a;|Oe^us^PoPY$Va~yi8|?+&=y$f+lABT<*pZr zP}D{~Pq1Qyni+@|aP;ixO~mbEW9#c0OU#YbDZIaw=_&$K%Ep2f%hO^&P67hApZe`x zv8b`Mz@?M_7-)b!lkQKk)JXXUuT|B8kJlvqRmRpxtQDgvrHMXC1B$M@Y%Me!BSx3P z#2Eawl$HleZhhTS6Txm>lN_+I`>eV$&v9fOg)%zVn3O5mI*lAl>QcHuW6!Kixmq`X zBCZ*Ck6OYtDiK!N47>jxI&O2a9x7M|i^IagRr-fmrmikEQGgw%J7bO|)*$2FW95O4 zeBs>KR)izRG1gRVL;F*sr8A}aRHO0gc$$j&ds8CIO1=Gwq1%_~E)CWNn9pCtBE}+`Jelk4{>S)M)`Ll=!~gnn1yq^EX(+y*ik@3Ou0qU`IgYi3*doM+5&dU!cho$pZ zn%lhKeZkS72P?Cf68<#kll_6OAO26bIbueZx**j6o;I0cS^XiL`y+>{cD}gd%lux} z)3N>MaE24WBZ}s0ApfdM;5J_Ny}rfUyxfkC``Awo2#sgLnGPewK};dORuT?@I6(5~ z?kE)Qh$L&fwJXzK){iYx!l5$Tt|^D~MkGZPA}(o6f7w~O2G6Vvzdo*a;iXzk$B66$ zwF#;wM7A+(;uFG4+UAY(2`*3XXx|V$K8AYu#ECJYSl@S=uZW$ksfC$~qrrbQj4??z-)uz0QL}>k^?fPnJTPw% zGz)~?B4}u0CzOf@l^um}HZzbaIwPmb<)< zi_3@E9lc)Qe2_`*Z^HH;1CXOceL=CHpHS{HySy3T%<^NrWQ}G0i4e1xm_K3(+~oi$ zoHl9wzb?Z4j#90DtURtjtgvi7uw8DzHYmtPb;?%8vb9n@bszT=1qr)V_>R%s!92_` zfnHQPANx z<#hIjIMm#*(v*!OXtF+w8kLu`o?VZ5k7{`vw{Yc^qYclpUGIM_PBN1+c{#Vxv&E*@ zxg=W2W~JuV{IuRYw3>LSI1)a!thID@R=bU+cU@DbR^_SXY`MC7HOsCN z!dO4OKV7(E_Z8T#8MA1H`99?Z!r0)qKW_#|29X3#Jb+5+>qUidbeP1NJ@)(qi2S-X zao|f0_tl(O+$R|Qwd$H{_ig|~I1fbp_$NkI!0E;Y z6JrnU{1Ra6^on{9gUUB0mwzP3S%B#h0fjo>JvV~#+X0P~JV=IG=yHG$O+p5O3NUgG zEQ}z6BTp^Fie)Sg<){Z&I8NwPR(=mO4joTLHkJ>|Tnk23E(Bo`FSbPc05lF2-+)X? z6vV3*m~IBHTy*^E!<0nA(tCOJW2G4DsH7)BxLV8kICn5lu6@U*R`w)o9;Ro$i8=Q^V%uH8n3q=+Yf;SFRZu z!+F&PKcH#8cG?aSK_Tl@K9P#8o+jry@gdexz&d(Q=47<7nw@e@FFfIRNL9^)1i@;A z28+$Z#rjv-wj#heI|<&J_DiJ*s}xd-f!{J8jfqOHE`TiHHZVIA8CjkNQ_u;Ery^^t zl1I75&u^`1_q)crO+JT4rx|z2ToSC>)Or@-D zy3S>jW*sNIZR-EBsfyaJ+Jq4BQE4?SePtD2+jY8*%FsSLZ9MY>+wk?}}}AFAw)vr{ml)8LUG-y9>^t!{~|sgpxYc0Gnkg`&~R z-pilJZjr@y5$>B=VMdZ73svct%##v%wdX~9fz6i3Q-zOKJ9wso+h?VME7}SjL=!NUG{J?M&i!>ma`eoEa@IX`5G>B1(7;%}M*%-# zfhJ(W{y;>MRz!Ic8=S}VaBKqh;~7KdnGEHxcL$kA-6E~=!hrN*zw9N+_=odt<$_H_8dbo;0=42wcAETPCVGUr~v(`Uai zb{=D!Qc!dOEU6v)2eHSZq%5iqK?B(JlCq%T6av$Cb4Rko6onlG&?CqaX7Y_C_cOC3 zYZ;_oI(}=>_07}Oep&Ws7x7-R)cc8zfe!SYxJYP``pi$FDS)4Fvw5HH=FiU6xfVqIM!hJ;Rx8c0cB7~aPtNH(Nmm5Vh{ibAoU#J6 zImRCr?(iyu_4W_6AWo3*vxTPUw@vPwy@E0`(>1Qi=%>5eSIrp^`` zK*Y?fK_6F1W>-7UsB)RPC4>>Ps9)f+^MqM}8AUm@tZ->j%&h1M8s*s!LX5&WxQcAh z8mciQej@RPm?660%>{_D+7er>%zX_{s|$Z+;G7_sfNfBgY(zLB4Ey}J9F>zX#K0f6 z?dVNIeEh?EIShmP6>M+d|0wMM85Sa4diw1hrg|ITJ}JDg@o8y>(rF9mXk5M z2@D|NA)-7>wD&wF;S_$KS=eE84`BGw3g0?6wGxu8ys4rwI?9U=*^VF22t3%mbGeOh z`!O-OpF7#Vceu~F`${bW0nYVU9ecmk31V{tF%iv&5hWofC>I~cqAt@u6|R+|HLMMX zVxuSlMFOK_EQ86#E8&KwxIr8S9tj_goWtLv4f@!&h8;Ov41{J~496vp9vX=(LK#j! zAwi*21RAV-LD>9Cw3bV_9X(X3)Kr0-UaB*7Y>t82EQ%!)(&(XuAYtTsYy-dz+w=$ir)VJpe!_$ z6SGpX^i(af3{o=VlFPC);|J8#(=_8#vdxDe|Cok+ANhYwbE*FO`Su2m1~w+&9<_9~ z-|tTU_ACGN`~CNW5WYYBn^B#SwZ(t4%3aPp z;o)|L6Rk569KGxFLUPx@!6OOa+5OjQLK5w&nAmwxkC5rZ|m&HT8G%GVZxB_@ME z>>{rnXUqyiJrT(8GMj_ap#yN_!9-lO5e8mR3cJiK3NE{_UM&=*vIU`YkiL$1%kf+1 z4=jk@7EEj`u(jy$HnzE33ZVW_J4bj}K;vT?T91YlO(|Y0FU4r+VdbmQ97%(J5 zkK*Bed8+C}FcZ@HIgdCMioV%A<*4pw_n}l*{Cr4}a(lq|injK#O?$tyvyE`S%(1`H z_wwRvk#13ElkZvij2MFGOj`fhy?nC^8`Zyo%yVcUAfEr8x&J#A{|moUBAV_^f$hpaUuyQeY3da^ zS9iRgf87YBwfe}>BO+T&Fl%rfpZh#+AM?Dq-k$Bq`vG6G_b4z%Kbd&v>qFjow*mBl z-OylnqOpLg}or7_VNwRg2za3VBK6FUfFX{|TD z`Wt0Vm2H$vdlRWYQJqDmM?JUbVqL*ZQY|5&sY*?!&%P8qhA~5+Af<{MaGo(dl&C5t zE%t!J0 zh6jqANt4ABdPxSTrVV}fLsRQal*)l&_*rFq(Ez}ClEH6LHv{J#v?+H-BZ2)Wy{K@9 z+ovXHq~DiDvm>O~r$LJo!cOuwL+Oa--6;UFE2q@g3N8Qkw5E>ytz^(&($!O47+i~$ zKM+tkAd-RbmP{s_rh+ugTD;lriL~`Xwkad#;_aM?nQ7L_muEFI}U_4$phjvYgleK~`Fo`;GiC07&Hq1F<%p;9Q;tv5b?*QnR%8DYJH3P>Svmv47Y>*LPZJy8_{9H`g6kQpyZU{oJ`m%&p~D=K#KpfoJ@ zn-3cqmHsdtN!f?~w+(t+I`*7GQA#EQC^lUA9(i6=i1PqSAc|ha91I%X&nXzjYaM{8$s&wEx@aVkQ6M{E2 zfzId#&r(XwUNtPcq4Ngze^+XaJA1EK-%&C9j>^9(secqe{}z>hR5CFNveMsVA)m#S zk)_%SidkY-XmMWlVnQ(mNJ>)ooszQ#vaK;!rPmGKXV7am^_F!Lz>;~{VrIO$;!#30XRhE1QqO_~#+Ux;B_D{Nk=grn z8Y0oR^4RqtcYM)7a%@B(XdbZCOqnX#fD{BQTeLvRHd(irHKq=4*jq34`6@VAQR8WG z^%)@5CXnD_T#f%@-l${>y$tfb>2LPmc{~5A82|16mH)R?&r#KKLs7xpN-D`=&Cm^R zvMA6#Ahr<3X>Q7|-qfTY)}32HkAz$_mibYV!I)u>bmjK`qwBe(>za^0Kt*HnFbSdO z1>+ryKCNxmm^)*$XfiDOF2|{-v3KKB?&!(S_Y=Ht@|ir^hLd978xuI&N{k>?(*f8H z=ClxVJK_%_z1TH0eUwm2J+2To7FK4o+n_na)&#VLn1m;!+CX+~WC+qg1?PA~KdOlC zW)C@pw75_xoe=w7i|r9KGIvQ$+3K?L{7TGHwrQM{dCp=Z*D}3kX7E-@sZnup!BImw z*T#a=+WcTwL78exTgBn|iNE3#EsOorO z*kt)gDzHiPt07fmisA2LWN?AymkdqTgr?=loT7z@d`wnlr6oN}@o|&JX!yPzC*Y8d zu6kWlTzE1)ckyBn+0Y^HMN+GA$wUO_LN6W>mxCo!0?oiQvT`z$jbSEu&{UHRU0E8# z%B^wOc@S!yhMT49Y)ww(Xta^8pmPCe@eI5C*ed96)AX9<>))nKx0(sci8gwob_1}4 z0DIL&vsJ1_s%<@y%U*-eX z5rN&(zef-5G~?@r79oZGW1d!WaTqQn0F6RIOa9tJ=0(kdd{d1{<*tHT#cCvl*i>YY zH+L7jq8xZNcTUBqj(S)ztTU!TM!RQ}In*n&Gn<>(60G7}4%WQL!o>hbJqNDSGwl#H z`4k+twp0cj%PsS+NKaxslAEu9!#U3xT1|_KB6`h=PI0SW`P9GTa7caD1}vKEglV8# zjKZR`pluCW19c2fM&ZG)c3T3Um;ir3y(tSCJ7Agl6|b524dy5El{^EQBG?E61H0XY z`bqg!;zhGhyMFl&(o=JWEJ8n~z)xI}A@C0d2hQGvw7nGv)?POU@(kS1m=%`|+^ika zXl8zjS?xqW$WlO?Ewa;vF~XbybHBor$f<%I&*t$F5fynwZlTGj|IjZtVfGa7l&tK} zW>I<69w(cZLu)QIVG|M2xzW@S+70NinQzk&Y0+3WT*cC)rx~04O-^<{JohU_&HL5XdUKW!uFy|i$FB|EMu0eUyW;gsf`XfIc!Z0V zeK&*hPL}f_cX=@iv>K%S5kL;cl_$v?n(Q9f_cChk8Lq$glT|=e+T*8O4H2n<=NGmn z+2*h+v;kBvF>}&0RDS>)B{1!_*XuE8A$Y=G8w^qGMtfudDBsD5>T5SB;Qo}fSkkiV ze^K^M(UthkwrD!&*tTsu>Dacdj_q`~V%r_twr$(Ct&_dKeeXE?fA&4&yASJWJ*}~- zel=@W)tusynfC_YqH4ll>4Eg`Xjs5F7Tj>tTLz<0N3)X<1px_d2yUY>X~y>>93*$) z5PuNMQLf9Bu?AAGO~a_|J2akO1M*@VYN^VxvP0F$2>;Zb9;d5Yfd8P%oFCCoZE$ z4#N$^J8rxYjUE_6{T%Y>MmWfHgScpuGv59#4u6fpTF%~KB^Ae`t1TD_^Ud#DhL+Dm zbY^VAM#MrAmFj{3-BpVSWph2b_Y6gCnCAombVa|1S@DU)2r9W<> zT5L8BB^er3zxKt1v(y&OYk!^aoQisqU zH(g@_o)D~BufUXcPt!Ydom)e|aW{XiMnes2z&rE?og>7|G+tp7&^;q?Qz5S5^yd$i z8lWr4g5nctBHtigX%0%XzIAB8U|T6&JsC4&^hZBw^*aIcuNO47de?|pGXJ4t}BB`L^d8tD`H`i zqrP8?#J@8T#;{^B!KO6J=@OWKhAerih(phML`(Rg7N1XWf1TN>=Z3Do{l_!d~DND&)O)D>ta20}@Lt77qSnVsA7>)uZAaT9bsB>u&aUQl+7GiY2|dAEg@%Al3i316y;&IhQL^8fw_nwS>f60M_-m+!5)S_6EPM7Y)(Nq^8gL7(3 zOiot`6Wy6%vw~a_H?1hLVzIT^i1;HedHgW9-P#)}Y6vF%C=P70X0Tk^z9Te@kPILI z_(gk!k+0%CG)%!WnBjjw*kAKs_lf#=5HXC00s-}oM-Q1aXYLj)(1d!_a7 z*Gg4Fe6F$*ujVjI|79Z5+Pr`us%zW@ln++2l+0hsngv<{mJ%?OfSo_3HJXOCys{Ug z00*YR-(fv<=&%Q!j%b-_ppA$JsTm^_L4x`$k{VpfLI(FMCap%LFAyq;#ns5bR7V+x zO!o;c5y~DyBPqdVQX)8G^G&jWkBy2|oWTw>)?5u}SAsI$RjT#)lTV&Rf8;>u*qXnb z8F%Xb=7#$m)83z%`E;49)t3fHInhtc#kx4wSLLms!*~Z$V?bTyUGiS&m>1P(952(H zuHdv=;o*{;5#X-uAyon`hP}d#U{uDlV?W?_5UjJvf%11hKwe&(&9_~{W)*y1nR5f_ z!N(R74nNK`y8>B!0Bt_Vr!;nc3W>~RiKtGSBkNlsR#-t^&;$W#)f9tTlZz>n*+Fjz z3zXZ;jf(sTM(oDzJt4FJS*8c&;PLTW(IQDFs_5QPy+7yhi1syPCarvqrHFcf&yTy)^O<1EBx;Ir`5W{TIM>{8w&PB>ro4;YD<5LF^TjTb0!zAP|QijA+1Vg>{Afv^% zmrkc4o6rvBI;Q8rj4*=AZacy*n8B{&G3VJc)so4$XUoie0)vr;qzPZVbb<#Fc=j+8CGBWe$n|3K& z_@%?{l|TzKSlUEO{U{{%Fz_pVDxs7i9H#bnbCw7@4DR=}r_qV!Zo~CvD4ZI*+j3kO zW6_=|S`)(*gM0Z;;}nj`73OigF4p6_NPZQ-Od~e$c_);;4-7sR>+2u$6m$Gf%T{aq zle>e3(*Rt(TPD}03n5)!Ca8Pu!V}m6v0o1;5<1h$*|7z|^(3$Y&;KHKTT}hV056wuF0Xo@mK-52~r=6^SI1NC%c~CC?n>yX6wPTgiWYVz!Sx^atLby9YNn1Rk{g?|pJaxD4|9cUf|V1_I*w zzxK)hRh9%zOl=*$?XUjly5z8?jPMy%vEN)f%T*|WO|bp5NWv@B(K3D6LMl!-6dQg0 zXNE&O>Oyf%K@`ngCvbGPR>HRg5!1IV$_}m@3dWB7x3t&KFyOJn9pxRXCAzFr&%37wXG;z^xaO$ekR=LJG ztIHpY8F5xBP{mtQidqNRoz= z@){+N3(VO5bD+VrmS^YjG@+JO{EOIW)9=F4v_$Ed8rZtHvjpiEp{r^c4F6Ic#ChlC zJX^DtSK+v(YdCW)^EFcs=XP7S>Y!4=xgmv>{S$~@h=xW-G4FF9?I@zYN$e5oF9g$# zb!eVU#J+NjLyX;yb)%SY)xJdvGhsnE*JEkuOVo^k5PyS=o#vq!KD46UTW_%R=Y&0G zFj6bV{`Y6)YoKgqnir2&+sl+i6foAn-**Zd1{_;Zb7Ki=u394C5J{l^H@XN`_6XTKY%X1AgQM6KycJ+= zYO=&t#5oSKB^pYhNdzPgH~aEGW2=ec1O#s-KG z71}LOg@4UEFtp3GY1PBemXpNs6UK-ax*)#$J^pC_me;Z$Je(OqLoh|ZrW*mAMBFn< zHttjwC&fkVfMnQeen8`Rvy^$pNRFVaiEN4Pih*Y3@jo!T0nsClN)pdrr9AYLcZxZ| zJ5Wlj+4q~($hbtuY zVQ7hl>4-+@6g1i`1a)rvtp-;b0>^`Dloy(#{z~ytgv=j4q^Kl}wD>K_Y!l~ zp(_&7sh`vfO(1*MO!B%<6E_bx1)&s+Ae`O)a|X=J9y~XDa@UB`m)`tSG4AUhoM=5& znWoHlA-(z@3n0=l{E)R-p8sB9XkV zZ#D8wietfHL?J5X0%&fGg@MH~(rNS2`GHS4xTo7L$>TPme+Is~!|79=^}QbPF>m%J zFMkGzSndiPO|E~hrhCeo@&Ea{M(ieIgRWMf)E}qeTxT8Q#g-!Lu*x$v8W^M^>?-g= zwMJ$dThI|~M06rG$Sv@C@tWR>_YgaG&!BAbkGggVQa#KdtDB)lMLNVLN|51C@F^y8 zCRvMB^{GO@j=cHfmy}_pCGbP%xb{pNN>? z?7tBz$1^zVaP|uaatYaIN+#xEN4jBzwZ|YI_)p(4CUAz1ZEbDk>J~Y|63SZaak~#0 zoYKruYsWHoOlC1(MhTnsdUOwQfz5p6-D0}4;DO$B;7#M{3lSE^jnTT;ns`>!G%i*F?@pR1JO{QTuD0U+~SlZxcc8~>IB{)@8p`P&+nDxNj`*gh|u?yrv$phpQcW)Us)bi`kT%qLj(fi{dWRZ%Es2!=3mI~UxiW0$-v3vUl?#g{p6eF zMEUAqo5-L0Ar(s{VlR9g=j7+lt!gP!UN2ICMokAZ5(Agd>})#gkA2w|5+<%-CuEP# zqgcM}u@3(QIC^Gx<2dbLj?cFSws_f3e%f4jeR?4M^M3cx1f+Qr6ydQ>n)kz1s##2w zk}UyQc+Z5G-d-1}{WzjkLXgS-2P7auWSJ%pSnD|Uivj5u!xk0 z_^-N9r9o;(rFDt~q1PvE#iJZ_f>J3gcP$)SOqhE~pD2|$=GvpL^d!r z6u=sp-CrMoF7;)}Zd7XO4XihC4ji?>V&(t^?@3Q&t9Mx=qex6C9d%{FE6dvU6%d94 zIE;hJ1J)cCqjv?F``7I*6bc#X)JW2b4f$L^>j{*$R`%5VHFi*+Q$2;nyieduE}qdS{L8y8F08yLs?w}{>8>$3236T-VMh@B zq-nujsb_1aUv_7g#)*rf9h%sFj*^mIcImRV*k~Vmw;%;YH(&ylYpy!&UjUVqqtfG` zox3esju?`unJJA_zKXRJP)rA3nXc$m^{S&-p|v|-0x9LHJm;XIww7C#R$?00l&Yyj z=e}gKUOpsImwW?N)+E(awoF@HyP^EhL+GlNB#k?R<2>95hz!h9sF@U20DHSB3~WMa zk90+858r@-+vWwkawJ)8ougd(i#1m3GLN{iSTylYz$brAsP%=&m$mQQrH$g%3-^VR zE%B`Vi&m8f3T~&myTEK28BDWCVzfWir1I?03;pX))|kY5ClO^+bae z*7E?g=3g7EiisYOrE+lA)2?Ln6q2*HLNpZEWMB|O-JI_oaHZB%CvYB(%=tU= zE*OY%QY58fW#RG5=gm0NR#iMB=EuNF@)%oZJ}nmm=tsJ?eGjia{e{yuU0l3{d^D@)kVDt=1PE)&tf_hHC%0MB znL|CRCPC}SeuVTdf>-QV70`0(EHizc21s^sU>y%hW0t!0&y<7}Wi-wGy>m%(-jsDj zP?mF|>p_K>liZ6ZP(w5(|9Ga%>tLgb$|doDDfkdW>Z z`)>V2XC?NJT26mL^@ zf+IKr27TfM!UbZ@?zRddC7#6ss1sw%CXJ4FWC+t3lHZupzM77m^=9 z&(a?-LxIq}*nvv)y?27lZ{j zifdl9hyJudyP2LpU$-kXctshbJDKS{WfulP5Dk~xU4Le4c#h^(YjJit4#R8_khheS z|8(>2ibaHES4+J|DBM7I#QF5u-*EdN{n=Kt@4Zt?@Tv{JZA{`4 zU#kYOv{#A&gGPwT+$Ud}AXlK3K7hYzo$(fBSFjrP{QQ zeaKg--L&jh$9N}`pu{Bs>?eDFPaWY4|9|foN%}i;3%;@4{dc+iw>m}{3rELqH21G! z`8@;w-zsJ1H(N3%|1B@#ioLOjib)j`EiJqPQVSbPSPVHCj6t5J&(NcWzBrzCiDt{4 zdlPAUKldz%6x5II1H_+jv)(xVL+a;P+-1hv_pM>gMRr%04@k;DTokASSKKhU1Qms| zrWh3a!b(J3n0>-tipg{a?UaKsP7?+|@A+1WPDiQIW1Sf@qDU~M_P65_s}7(gjTn0X zucyEm)o;f8UyshMy&>^SC3I|C6jR*R_GFwGranWZe*I>K+0k}pBuET&M~ z;Odo*ZcT?ZpduHyrf8E%IBFtv;JQ!N_m>!sV6ly$_1D{(&nO~w)G~Y`7sD3#hQk%^ zp}ucDF_$!6DAz*PM8yE(&~;%|=+h(Rn-=1Wykas_-@d&z#=S}rDf`4w(rVlcF&lF! z=1)M3YVz7orwk^BXhslJ8jR);sh^knJW(Qmm(QdSgIAIdlN4Te5KJisifjr?eB{FjAX1a0AB>d?qY4Wx>BZ8&}5K0fA+d{l8 z?^s&l8#j7pR&ijD?0b%;lL9l$P_mi2^*_OL+b}4kuLR$GAf85sOo02?Y#90}CCDiS zZ%rbCw>=H~CBO=C_JVV=xgDe%b4FaEFtuS7Q1##y686r%F6I)s-~2(}PWK|Z8M+Gu zl$y~5@#0Ka%$M<&Cv%L`a8X^@tY&T7<0|(6dNT=EsRe0%kp1Qyq!^43VAKYnr*A5~ zsI%lK1ewqO;0TpLrT9v}!@vJK{QoVa_+N4FYT#h?Y8rS1S&-G+m$FNMP?(8N`MZP zels(*?kK{{^g9DOzkuZXJ2;SrOQsp9T$hwRB1(phw1c7`!Q!by?Q#YsSM#I12RhU{$Q+{xj83axHcftEc$mNJ8_T7A-BQc*k(sZ+~NsO~xAA zxnbb%dam_fZlHvW7fKXrB~F&jS<4FD2FqY?VG?ix*r~MDXCE^WQ|W|WM;gsIA4lQP zJ2hAK@CF*3*VqPr2eeg6GzWFlICi8S>nO>5HvWzyZTE)hlkdC_>pBej*>o0EOHR|) z$?};&I4+_?wvL*g#PJ9)!bc#9BJu1(*RdNEn>#Oxta(VWeM40ola<0aOe2kSS~{^P zDJBd}0L-P#O-CzX*%+$#v;(x%<*SPgAje=F{Zh-@ucd2DA(yC|N_|ocs*|-!H%wEw z@Q!>siv2W;C^^j^59OAX03&}&D*W4EjCvfi(ygcL#~t8XGa#|NPO+*M@Y-)ctFA@I z-p7npT1#5zOLo>7q?aZpCZ=iecn3QYklP;gF0bq@>oyBq94f6C=;Csw3PkZ|5q=(c zfs`aw?II0e(h=|7o&T+hq&m$; zBrE09Twxd9BJ2P+QPN}*OdZ-JZV7%av@OM7v!!NL8R;%WFq*?{9T3{ct@2EKgc8h) zMxoM$SaF#p<`65BwIDfmXG6+OiK0e)`I=!A3E`+K@61f}0e z!2a*FOaDrOe>U`q%K!QN`&=&0C~)CaL3R4VY(NDt{Xz(Xpqru5=r#uQN1L$Je1*dkdqQ*=lofQaN%lO!<5z9ZlHgxt|`THd>2 zsWfU$9=p;yLyJyM^t zS2w9w?Bpto`@H^xJpZDKR1@~^30Il6oFGfk5%g6w*C+VM)+%R@gfIwNprOV5{F^M2 zO?n3DEzpT+EoSV-%OdvZvNF+pDd-ZVZ&d8 zKeIyrrfPN=EcFRCPEDCVflX#3-)Ik_HCkL(ejmY8vzcf-MTA{oHk!R2*36`O68$7J zf}zJC+bbQk--9Xm!u#lgLvx8TXx2J258E5^*IZ(FXMpq$2LUUvhWQPs((z1+2{Op% z?J}9k5^N=z;7ja~zi8a_-exIqWUBJwohe#4QJ`|FF*$C{lM18z^#hX6!5B8KAkLUX ziP=oti-gpV(BsLD{0(3*dw}4JxK23Y7M{BeFPucw!sHpY&l%Ws4pSm`+~V7;bZ%Dx zeI)MK=4vC&5#;2MT7fS?^ch9?2;%<8Jlu-IB&N~gg8t;6S-#C@!NU{`p7M8@2iGc& zg|JPg%@gCoCQ&s6JvDU&`X2S<57f(k8nJ1wvBu{8r?;q3_kpZZ${?|( z+^)UvR33sjSd)aT!UPkA;ylO6{aE3MQa{g%Mcf$1KONcjO@&g5zPHWtzM1rYC{_K> zgQNcs<{&X{OA=cEWw5JGqpr0O>x*Tfak2PE9?FuWtz^DDNI}rwAaT0(bdo-<+SJ6A z&}S%boGMWIS0L}=S>|-#kRX;e^sUsotry(MjE|3_9duvfc|nwF#NHuM-w7ZU!5ei8 z6Mkf>2)WunY2eU@C-Uj-A zG(z0Tz2YoBk>zCz_9-)4a>T46$(~kF+Y{#sA9MWH%5z#zNoz)sdXq7ZR_+`RZ%0(q zC7&GyS_|BGHNFl8Xa%@>iWh%Gr?=J5<(!OEjauj5jyrA-QXBjn0OAhJJ9+v=!LK`` z@g(`^*84Q4jcDL`OA&ZV60djgwG`|bcD*i50O}Q{9_noRg|~?dj%VtKOnyRs$Uzqg z191aWoR^rDX#@iSq0n z?9Sg$WSRPqSeI<}&n1T3!6%Wj@5iw5`*`Btni~G=&;J+4`7g#OQTa>u`{4ZZ(c@s$ zK0y;ySOGD-UTjREKbru{QaS>HjN<2)R%Nn-TZiQ(Twe4p@-saNa3~p{?^V9Nixz@a zykPv~<@lu6-Ng9i$Lrk(xi2Tri3q=RW`BJYOPC;S0Yly%77c727Yj-d1vF!Fuk{Xh z)lMbA69y7*5ufET>P*gXQrxsW+ zz)*MbHZv*eJPEXYE<6g6_M7N%#%mR{#awV3i^PafNv(zyI)&bH?F}2s8_rR(6%!V4SOWlup`TKAb@ee>!9JKPM=&8g#BeYRH9FpFybxBXQI2|g}FGJfJ+ zY-*2hB?o{TVL;Wt_ek;AP5PBqfDR4@Z->_182W z{P@Mc27j6jE*9xG{R$>6_;i=y{qf(c`5w9fa*`rEzX6t!KJ(p1H|>J1pC-2zqWENF zmm=Z5B4u{cY2XYl(PfrInB*~WGWik3@1oRhiMOS|D;acnf-Bs(QCm#wR;@Vf!hOPJ zgjhDCfDj$HcyVLJ=AaTbQ{@vIv14LWWF$=i-BDoC11}V;2V8A`S>_x)vIq44-VB-v z*w-d}$G+Ql?En8j!~ZkCpQ$|cA0|+rrY>tiCeWxkRGPoarxlGU2?7%k#F693RHT24 z-?JsiXlT2PTqZqNb&sSc>$d;O4V@|b6VKSWQb~bUaWn1Cf0+K%`Q&Wc<>mQ>*iEGB zbZ;aYOotBZ{vH3y<0A*L0QVM|#rf*LIsGx(O*-7)r@yyBIzJnBFSKBUSl1e|8lxU* zzFL+YDVVkIuzFWeJ8AbgN&w(4-7zbiaMn{5!JQXu)SELk*CNL+Fro|2v|YO)1l15t zs(0^&EB6DPMyaqvY>=KL>)tEpsn;N5Q#yJj<9}ImL((SqErWN3Q=;tBO~ExTCs9hB z2E$7eN#5wX4<3m^5pdjm#5o>s#eS_Q^P)tm$@SawTqF*1dj_i#)3};JslbLKHXl_N z)Fxzf>FN)EK&Rz&*|6&%Hs-^f{V|+_vL1S;-1K-l$5xiC@}%uDuwHYhmsV?YcOUlk zOYkG5v2+`+UWqpn0aaaqrD3lYdh0*!L`3FAsNKu=Q!vJu?Yc8n|CoYyDo_`r0mPoo z8>XCo$W4>l(==h?2~PoRR*kEe)&IH{1sM41mO#-36`02m#nTX{r*r`Q5rZ2-sE|nA zhnn5T#s#v`52T5|?GNS`%HgS2;R(*|^egNPDzzH_z^W)-Q98~$#YAe)cEZ%vge965AS_am#DK#pjPRr-!^za8>`kksCAUj(Xr*1NW5~e zpypt_eJpD&4_bl_y?G%>^L}=>xAaV>KR6;^aBytqpiHe%!j;&MzI_>Sx7O%F%D*8s zSN}cS^<{iiK)=Ji`FpO#^zY!_|D)qeRNAtgmH)m;qC|mq^j(|hL`7uBz+ULUj37gj zksdbnU+LSVo35riSX_4z{UX=%n&}7s0{WuZYoSfwAP`8aKN9P@%e=~1`~1ASL-z%# zw>DO&ixr}c9%4InGc*_y42bdEk)ZdG7-mTu0bD@_vGAr*NcFoMW;@r?@LUhRI zCUJgHb`O?M3!w)|CPu~ej%fddw20lod?Ufp8Dmt0PbnA0J%KE^2~AIcnKP()025V> zG>noSM3$5Btmc$GZoyP^v1@Poz0FD(6YSTH@aD0}BXva?LphAiSz9f&Y(aDAzBnUh z?d2m``~{z;{}kZJ>a^wYI?ry(V9hIoh;|EFc0*-#*`$T0DRQ1;WsqInG;YPS+I4{g zJGpKk%%Sdc5xBa$Q^_I~(F97eqDO7AN3EN0u)PNBAb+n+ zWBTxQx^;O9o0`=g+Zrt_{lP!sgWZHW?8bLYS$;1a@&7w9rD9|Ge;Gb?sEjFoF9-6v z#!2)t{DMHZ2@0W*fCx;62d#;jouz`R5Y(t{BT=$N4yr^^o$ON8d{PQ=!O zX17^CrdM~7D-;ZrC!||<+FEOxI_WI3CA<35va%4v>gc zEX-@h8esj=a4szW7x{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1* znV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI z##W$P9M{B3c3Si9gw^jlPU-JqD~Cye;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP> zrp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ueg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{ zlB`9HUl-WWCG|<1XANN3JVAkRYvr5U4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvx zK%p23>M&=KTCgR!Ee8c?DAO2_R?B zkaqr6^BSP!8dHXxj%N1l+V$_%vzHjqvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rU zHfcog>kv3UZAEB*g7Er@t6CF8kHDmKTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B zZ+jjWgjJ!043F+&#_;D*mz%Q60=L9Ove|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw- z19qI#oB(RSNydn0t~;tAmK!P-d{b-@@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^8 z2zk8VXx|>#R^JCcWdBCy{0nPmYFOxN55#^-rlqobe0#L6)bi?E?SPymF*a5oDDeSd zO0gx?#KMoOd&G(2O@*W)HgX6y_aa6iMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H z`oa=g0SyiLd~BxAj2~l$zRSDHxvDs;I4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*( ze-417=bO2q{492SWrqDK+L3#ChUHtz*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEX zATx4K*hcO`sY$jk#jN5WD<=C3nvuVsRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_ zl3F^#f_rDu8l}l8qcAz0FFa)EAt32IUy_JLIhU_J^l~FRH&6-ivSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPm zZi-noqS!^Ftb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@ zfFGJtW3r>qV>1Z0r|L>7I3un^gcep$AAWfZHRvB|E*kktY$qQP_$YG60C@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn` zEgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czP zg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-&SFp;!k?uFayytV$8HPwuyELSXOs^27XvK-D zOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2S43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@ zK^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf z9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^&X%=?`6lCy~?`&WSWt z?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6VjA#>1f@EYiS8MRHZphp zMA_5`znM=pzUpBPO)pXGYpQ6gkine{6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ z<1SE2Edkfk9C!0t%}8Yio09^F`YGzpaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8p zT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{eSyybt)m<=zXoA^RALYG-2t zouH|L*BLvmm9cdMmn+KGopyR@4*=&0&4g|FLoreZOhRmh=)R0bg~ zT2(8V_q7~42-zvb)+y959OAv!V$u(O3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+ zMWQoJI_r$HxL5km1#6(e@{lK3Udc~n0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai< z6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF# zMnbr-f55(cTa^q4+#)=s+ThMaV~E`B8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg% zbOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$18Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9Sq zuGh<9<=AO&g6BZte6hn>Qmvv;Rt)*cJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapi zPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wB zxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5o}_(P;=!y-AjFrERh%8la!z6Fn@lR?^E~H12D?8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2 zwG1|5ikb^qHv&9hT8w83+yv&BQXOQyMVJSBL(Ky~p)gU3#%|blG?IR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-} z9?*x{y(`509qhCV*B47f2hLrGl^<@SuRGR!KwHei?!CM10Tq*YDIoBNyRuO*>3FU? zHjipIE#B~y3FSfOsMfj~F9PNr*H?0oHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R% zrq|ic4fzJ#USpTm;X7K+E%xsT_3VHKe?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>Jm ziU#?2^`>arnsl#)*R&nf_%>A+qwl%o{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVD zM8AI6MM2V*^_M^sQ0dmHu11fy^kOqXqzpr?K$`}BKWG`=Es(9&S@K@)ZjA{lj3ea7_MBP zk(|hBFRjHVMN!sNUkrB;(cTP)T97M$0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5 zI7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIo zIZSVls9kFGsTwvr4{T_LidcWtt$u{kJlW7moRaH6+A5hW&;;2O#$oKyEN8kx`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41Uw z`P+tft^E2B$domKT@|nNW`EHwyj>&}K;eDpe z1bNOh=fvIfk`&B61+S8ND<(KC%>y&?>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xo zaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$itm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H z?n6^}l{D``Me90`^o|q!olsF?UX3YSq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfw zR!gX_%AR=L3BFsf8LxI|K^J}deh0ZdV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z z-G6kzA01M?rba+G_mwNMQD1mbVbNTWmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bA zv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$ z8p_}t*XIOehezolNa-a2x0BS})Y9}&*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWK zDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~VCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjMsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3 z-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$)WL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>I zgy8p#i4GN{>#v=pFYUQT(g&b$OeTy-X_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6< znXs{W!bkP|s_YI*Yx%4stI`=ZO45IK6rBs`g7sP40ic}GZ58s?Mc$&i`kq_tfci>N zIHrC0H+Qpam1bNa=(`SRKjixBTtm&e`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_ z%7SUeH6=TrXt3J@js`4iDD0=IoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bUpX9ATD#moByY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOx zXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+pmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X z?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L z*&?(77!-=zvnCVW&kUcZMb6;2!83si518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j( ziTaS4HhQ)ldR=r)_7vYFUr%THE}cPF{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVA zdDZRybv?H|>`9f$AKVjFWJ=wegO7hOOIYCtd?Vj{EYLT*^gl35|HQ`R=ti+ADm{jyQE7K@kdjuqJhWVSks>b^ zxha88-h3s;%3_5b1TqFCPTxVjvuB5U>v=HyZ$?JSk+&I%)M7KE*wOg<)1-Iy)8-K! z^XpIt|0ibmk9RtMmlUd7#Ap3Q!q9N4atQy)TmrhrFhfx1DAN`^vq@Q_SRl|V z#lU<~n67$mT)NvHh`%als+G-)x1`Y%4Bp*6Un5Ri9h=_Db zA-AdP!f>f0m@~>7X#uBM?diI@)Egjuz@jXKvm zJo+==juc9_<;CqeRaU9_Mz@;3e=E4=6TK+c`|uu#pIqhSyNm`G(X)&)B`8q0RBv#> z`gGlw(Q=1Xmf55VHj%C#^1lpc>LY8kfA@|rlC1EA<1#`iuyNO z(=;irt{_&K=i4)^x%;U(Xv<)+o=dczC5H3W~+e|f~{*ucxj@{Yi-cw^MqYr3fN zF5D+~!wd$#al?UfMnz(@K#wn`_5na@rRr8XqN@&M&FGEC@`+OEv}sI1hw>Up0qAWf zL#e4~&oM;TVfjRE+10B_gFlLEP9?Q-dARr3xi6nQqnw>k-S;~b z;!0s2VS4}W8b&pGuK=7im+t(`nz@FnT#VD|!)eQNp-W6)@>aA+j~K*H{$G`y2|QHY z|Hmy+CR@#jWY4~)lr1qBJB_RfHJFfP<}pK5(#ZZGSqcpyS&}01LnTWk5fzmXMGHkJ zTP6L^B+uj;lmB_W<~4=${+v0>z31M!-_O@o-O9GyW)j_mjx}!0@br_LE-7SIuPP84 z;5=O(U*g_um0tyG|61N@d9lEuOeiRd+#NY^{nd5;-CVlw&Ap7J?qwM^?E29wvS}2d zbzar4Fz&RSR(-|s!Z6+za&Z zY#D<5q_JUktIzvL0)yq_kLWG6DO{ri=?c!y!f(Dk%G{8)k`Gym%j#!OgXVDD3;$&v@qy#ISJfp=Vm>pls@9-mapVQChAHHd-x+OGx)(*Yr zC1qDUTZ6mM(b_hi!TuFF2k#8uI2;kD70AQ&di$L*4P*Y-@p`jdm%_c3f)XhYD^6M8&#Y$ZpzQMcR|6nsH>b=*R_Von!$BTRj7yGCXokoAQ z&ANvx0-Epw`QIEPgI(^cS2f(Y85yV@ygI{ewyv5Frng)e}KCZF7JbR(&W618_dcEh(#+^zZFY;o<815<5sOHQdeax9_!PyM&;{P zkBa5xymca0#)c#tke@3KNEM8a_mT&1gm;p&&JlMGH(cL(b)BckgMQ^9&vRwj!~3@l zY?L5}=Jzr080OGKb|y`ee(+`flQg|!lo6>=H)X4`$Gz~hLmu2a%kYW_Uu8x09Pa0J zKZ`E$BKJ=2GPj_3l*TEcZ*uYRr<*J^#5pILTT;k_cgto1ZL-%slyc16J~OH-(RgDA z%;EjEnoUkZ&acS{Q8`{i6T5^nywgqQI5bDIymoa7CSZG|WWVk>GM9)zy*bNih|QIm z%0+(Nnc*a_xo;$=!HQYaapLms>J1ToyjtFByY`C2H1wT#178#4+|{H0BBqtCdd$L% z_3Hc60j@{t9~MjM@LBalR&6@>B;9?r<7J~F+WXyYu*y3?px*=8MAK@EA+jRX8{CG?GI-< z54?Dc9CAh>QTAvyOEm0^+x;r2BWX|{3$Y7)L5l*qVE*y0`7J>l2wCmW zL1?|a`pJ-l{fb_N;R(Z9UMiSj6pQjOvQ^%DvhIJF!+Th7jO2~1f1N+(-TyCFYQZYw z4)>7caf^Ki_KJ^Zx2JUb z&$3zJy!*+rCV4%jqwyuNY3j1ZEiltS0xTzd+=itTb;IPYpaf?8Y+RSdVdpacB(bVQ zC(JupLfFp8y43%PMj2}T|VS@%LVp>hv4Y!RPMF?pp8U_$xCJ)S zQx!69>bphNTIb9yn*_yfj{N%bY)t{L1cs8<8|!f$;UQ*}IN=2<6lA;x^(`8t?;+ST zh)z4qeYYgZkIy{$4x28O-pugO&gauRh3;lti9)9Pvw+^)0!h~%m&8Q!AKX%urEMnl z?yEz?g#ODn$UM`+Q#$Q!6|zsq_`dLO5YK-6bJM6ya>}H+vnW^h?o$z;V&wvuM$dR& zeEq;uUUh$XR`TWeC$$c&Jjau2it3#%J-y}Qm>nW*s?En?R&6w@sDXMEr#8~$=b(gk zwDC3)NtAP;M2BW_lL^5ShpK$D%@|BnD{=!Tq)o(5@z3i7Z){} zGr}Exom_qDO{kAVkZ*MbLNHE666Kina#D{&>Jy%~w7yX$oj;cYCd^p9zy z8*+wgSEcj$4{WxKmCF(5o7U4jqwEvO&dm1H#7z}%VXAbW&W24v-tS6N3}qrm1OnE)fUkoE8yMMn9S$?IswS88tQWm4#Oid#ckgr6 zRtHm!mfNl-`d>O*1~d7%;~n+{Rph6BBy^95zqI{K((E!iFQ+h*C3EsbxNo_aRm5gj zKYug($r*Q#W9`p%Bf{bi6;IY0v`pB^^qu)gbg9QHQ7 zWBj(a1YSu)~2RK8Pi#C>{DMlrqFb9e_RehEHyI{n?e3vL_}L>kYJC z_ly$$)zFi*SFyNrnOt(B*7E$??s67EO%DgoZL2XNk8iVx~X_)o++4oaK1M|ou73vA0K^503j@uuVmLcHH4ya-kOIDfM%5%(E z+Xpt~#7y2!KB&)PoyCA+$~DXqxPxxALy!g-O?<9+9KTk4Pgq4AIdUkl`1<1#j^cJg zgU3`0hkHj_jxV>`Y~%LAZl^3o0}`Sm@iw7kwff{M%VwtN)|~!p{AsfA6vB5UolF~d zHWS%*uBDt<9y!9v2Xe|au&1j&iR1HXCdyCjxSgG*L{wmTD4(NQ=mFjpa~xooc6kju z`~+d{j7$h-;HAB04H!Zscu^hZffL#9!p$)9>sRI|Yovm)g@F>ZnosF2EgkU3ln0bR zTA}|+E(tt)!SG)-bEJi_0m{l+(cAz^pi}`9=~n?y&;2eG;d9{M6nj>BHGn(KA2n|O zt}$=FPq!j`p&kQ8>cirSzkU0c08%8{^Qyqi-w2LoO8)^E7;;I1;HQ6B$u0nNaX2CY zSmfi)F`m94zL8>#zu;8|{aBui@RzRKBlP1&mfFxEC@%cjl?NBs`cr^nm){>;$g?rhKr$AO&6qV_Wbn^}5tfFBry^e1`%du2~o zs$~dN;S_#%iwwA_QvmMjh%Qo?0?rR~6liyN5Xmej8(*V9ym*T`xAhHih-v$7U}8=dfXi2i*aAB!xM(Xekg*ix@r|ymDw*{*s0?dlVys2e)z62u1 z+k3esbJE=-P5S$&KdFp+2H7_2e=}OKDrf( z9-207?6$@f4m4B+9E*e((Y89!q?zH|mz_vM>kp*HGXldO0Hg#!EtFhRuOm$u8e~a9 z5(roy7m$Kh+zjW6@zw{&20u?1f2uP&boD}$#Zy)4o&T;vyBoqFiF2t;*g=|1=)PxB z8eM3Mp=l_obbc?I^xyLz?4Y1YDWPa+nm;O<$Cn;@ane616`J9OO2r=rZr{I_Kizyc zP#^^WCdIEp*()rRT+*YZK>V@^Zs=ht32x>Kwe zab)@ZEffz;VM4{XA6e421^h~`ji5r%)B{wZu#hD}f3$y@L0JV9f3g{-RK!A?vBUA}${YF(vO4)@`6f1 z-A|}e#LN{)(eXloDnX4Vs7eH|<@{r#LodP@Nz--$Dg_Par%DCpu2>2jUnqy~|J?eZ zBG4FVsz_A+ibdwv>mLp>P!(t}E>$JGaK$R~;fb{O3($y1ssQQo|5M;^JqC?7qe|hg zu0ZOqeFcp?qVn&Qu7FQJ4hcFi&|nR!*j)MF#b}QO^lN%5)4p*D^H+B){n8%VPUzi! zDihoGcP71a6!ab`l^hK&*dYrVYzJ0)#}xVrp!e;lI!+x+bfCN0KXwUAPU9@#l7@0& QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 54708 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

_x$+l{{cH8$W#CT diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20c531b95..59250647c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Dec 03 19:20:35 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip diff --git a/gradlew b/gradlew index cccdd3d51..1b6c78733 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done fi +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f9553162f..107acd32c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell