From 2af13a4e6ca6391137348c46ec230f0e2c0b047e Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 6 Oct 2020 09:40:16 +0530 Subject: [PATCH 01/11] Artist fallback Fallback to fetch Album cover for missing artists --- .../retromusic/glide/ArtistGlideRequest.java | 2 +- .../glide/artistimage/ArtistImageLoader.kt | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java index 4d9d2124..c11ffcc2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java +++ b/app/src/main/java/code/name/monkey/retromusic/glide/ArtistGlideRequest.java @@ -62,7 +62,7 @@ public class ArtistGlideRequest { boolean hasCustomImage = CustomArtistImageUtil.Companion.getInstance(App.Companion.getContext()) .hasCustomArtistImage(artist); if (noCustomImage || !hasCustomImage) { - return requestManager.load(new ArtistImage(artist.getName())); + return requestManager.load(new ArtistImage(artist)); } else { return requestManager.load(CustomArtistImageUtil.getFile(artist)); } diff --git a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt index 43d23120..9b67cb62 100644 --- a/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/glide/artistimage/ArtistImageLoader.kt @@ -15,6 +15,7 @@ package code.name.monkey.retromusic.glide.artistimage import android.content.Context +import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Data import code.name.monkey.retromusic.network.DeezerService import code.name.monkey.retromusic.util.MusicUtil @@ -34,7 +35,7 @@ import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit -class ArtistImage(val artistName: String) +class ArtistImage(val artist: Artist) class ArtistImageFetcher( private val context: Context, @@ -53,7 +54,7 @@ class ArtistImageFetcher( } override fun getId(): String { - return model.artistName + return model.artist.name } override fun cancel() { @@ -62,10 +63,10 @@ class ArtistImageFetcher( } override fun loadData(priority: Priority?): InputStream? { - if (!MusicUtil.isArtistNameUnknown(model.artistName) && + if (!MusicUtil.isArtistNameUnknown(model.artist.name) && PreferenceUtil.isAllowedToDownloadMetadata() ) { - val artists = model.artistName.split(",") + val artists = model.artist.name.split(",") val response = deezerService.getArtistImage(artists[0]).execute() if (!response.isSuccessful) { @@ -85,13 +86,21 @@ class ArtistImageFetcher( val glideUrl = GlideUrl(imageUrl) urlFetcher = urlLoader.getResourceFetcher(glideUrl, width, height) urlFetcher?.loadData(priority) - } else null + } else { + getFallbackAlbumImage() + } } catch (e: Exception) { - null + getFallbackAlbumImage() } } else return null } + private fun getFallbackAlbumImage(): InputStream? { + val imageUri = MusicUtil.getMediaStoreAlbumCoverUri(model.artist.safeGetFirstAlbum().id) + return context.contentResolver.openInputStream(imageUri) + } + + private fun getHighestQuality(imageUrl: Data): String { return when { imageUrl.pictureXl.isNotEmpty() -> imageUrl.pictureXl From defcd86152b01362508524e3a0e159aff518d259 Mon Sep 17 00:00:00 2001 From: Hemanth S Date: Tue, 6 Oct 2020 14:16:04 +0530 Subject: [PATCH 02/11] Add Spotless --- app/build.gradle | 4 +- .../java/code/name/monkey/retromusic/App.kt | 10 +- .../code/name/monkey/retromusic/Constants.kt | 18 +- .../name/monkey/retromusic/HomeSection.kt | 16 +- .../retromusic/LanguageContextWrapper.java | 58 +- .../retromusic/RetroBottomSheetBehavior.java | 44 +- .../activities/DriveModeActivity.kt | 12 +- .../activities/LicenseActivity.java | 126 +- .../activities/LockScreenActivity.kt | 16 +- .../retromusic/activities/LyricsActivity.kt | 23 +- .../retromusic/activities/MainActivity.kt | 24 +- .../activities/PermissionActivity.kt | 19 +- .../activities/PlayingQueueActivity.kt | 16 +- .../retromusic/activities/PurchaseActivity.kt | 17 +- .../retromusic/activities/SettingsActivity.kt | 17 +- .../activities/ShareInstagramStory.kt | 10 +- .../activities/SupportDevelopmentActivity.kt | 25 +- .../retromusic/activities/UserInfoActivity.kt | 26 +- .../activities/WhatsNewActivity.java | 151 +- .../activities/base/AbsBaseActivity.kt | 18 +- .../base/AbsMusicServiceActivity.kt | 25 +- .../base/AbsSlidingMusicPanelActivity.kt | 20 +- .../activities/base/AbsThemeActivity.kt | 17 +- .../activities/bugreport/BugReportActivity.kt | 17 +- .../bugreport/model/DeviceInfo.java | 260 +- .../activities/bugreport/model/Report.java | 41 +- .../bugreport/model/github/ExtraInfo.java | 97 +- .../bugreport/model/github/GithubLogin.java | 51 +- .../bugreport/model/github/GithubTarget.java | 24 +- .../activities/saf/SAFGuideActivity.java | 85 +- .../tageditor/AbsTagEditorActivity.kt | 22 +- .../tageditor/AlbumTagEditorActivity.kt | 18 +- .../tageditor/SongTagEditorActivity.kt | 18 +- .../tageditor/WriteTagsAsyncTask.java | 312 +-- .../adapter/CategoryInfoAdapter.java | 173 +- .../retromusic/adapter/ContributorAdapter.kt | 14 + .../monkey/retromusic/adapter/GenreAdapter.kt | 14 + .../monkey/retromusic/adapter/HomeAdapter.kt | 16 +- .../retromusic/adapter/SearchAdapter.kt | 18 +- .../retromusic/adapter/SongFileAdapter.kt | 24 +- .../retromusic/adapter/TranslatorsAdapter.kt | 16 +- .../retromusic/adapter/album/AlbumAdapter.kt | 17 +- .../adapter/album/AlbumCoverPagerAdapter.kt | 15 +- .../adapter/album/HorizontalAlbumAdapter.kt | 18 +- .../adapter/artist/ArtistAdapter.kt | 19 +- .../adapter/base/AbsMultiSelectAdapter.java | 197 +- .../adapter/base/MediaEntryViewHolder.java | 152 +- .../adapter/playlist/LegacyPlaylistAdapter.kt | 16 +- .../adapter/playlist/PlaylistAdapter.kt | 18 +- .../adapter/song/AbsOffsetSongAdapter.kt | 16 +- .../song/OrderablePlaylistSongAdapter.kt | 14 + .../adapter/song/PlayingQueueAdapter.kt | 22 +- .../adapter/song/PlaylistSongAdapter.kt | 16 +- .../adapter/song/ShuffleButtonSongAdapter.kt | 16 +- .../adapter/song/SimpleSongAdapter.kt | 14 + .../retromusic/adapter/song/SongAdapter.kt | 14 + .../appshortcuts/AppShortcutIconGenerator.kt | 15 +- .../AppShortcutLauncherActivity.kt | 10 +- .../appshortcuts/DynamicShortcutManager.kt | 14 +- .../shortcuttype/BaseShortcutType.kt | 10 +- .../shortcuttype/LastAddedShortcutType.kt | 10 +- .../shortcuttype/ShuffleAllShortcutType.kt | 10 +- .../shortcuttype/TopTracksShortcutType.kt | 10 +- .../retromusic/appwidgets/AppWidgetBig.kt | 11 +- .../retromusic/appwidgets/AppWidgetCard.kt | 10 +- .../retromusic/appwidgets/AppWidgetClassic.kt | 11 +- .../retromusic/appwidgets/AppWidgetSmall.kt | 10 +- .../retromusic/appwidgets/AppWidgetText.kt | 13 +- .../retromusic/appwidgets/BootReceiver.kt | 10 +- .../appwidgets/base/BaseAppWidget.kt | 28 +- .../monkey/retromusic/db/BlackListStoreDao.kt | 16 +- .../retromusic/db/BlackListStoreEntity.kt | 16 +- .../name/monkey/retromusic/db/HistoryDao.kt | 16 +- .../monkey/retromusic/db/HistoryEntity.kt | 16 +- .../name/monkey/retromusic/db/LyricsDao.kt | 16 +- .../name/monkey/retromusic/db/LyricsEntity.kt | 16 +- .../name/monkey/retromusic/db/PlayCountDao.kt | 16 +- .../monkey/retromusic/db/PlayCountEntity.kt | 16 +- .../name/monkey/retromusic/db/PlaylistDao.kt | 19 +- .../monkey/retromusic/db/PlaylistEntity.kt | 16 +- .../monkey/retromusic/db/PlaylistWithSongs.kt | 15 +- .../monkey/retromusic/db/RetroDatabase.kt | 16 +- .../name/monkey/retromusic/db/SongEntity.kt | 15 +- .../monkey/retromusic/db/SongExtension.kt | 15 +- .../retromusic/dialogs/AddToPlaylistDialog.kt | 17 +- .../dialogs/BlacklistFolderChooserDialog.java | 247 +- .../dialogs/CreatePlaylistDialog.kt | 16 +- .../dialogs/DeletePlaylistDialog.kt | 17 +- .../retromusic/dialogs/DeleteSongsDialog.kt | 16 +- .../dialogs/ImportPlaylistDialog.kt | 16 +- .../dialogs/RemoveSongFromPlaylistDialog.kt | 16 +- .../dialogs/RenamePlaylistDialog.kt | 16 +- .../retromusic/dialogs/SavePlaylistDialog.kt | 18 +- .../retromusic/dialogs/SleepTimerDialog.kt | 13 +- .../retromusic/dialogs/SongDetailDialog.kt | 14 +- .../retromusic/dialogs/SongShareDialog.kt | 10 +- .../retromusic/extensions/ActivityEx.kt | 12 +- .../monkey/retromusic/extensions/ColorExt.kt | 12 +- .../retromusic/extensions/CursorExtensions.kt | 16 +- .../retromusic/extensions/DialogExtension.kt | 16 +- .../retromusic/extensions/DimenExtension.kt | 16 +- .../retromusic/extensions/DrawableExt.kt | 12 +- .../retromusic/extensions/FragmentExt.kt | 18 +- .../extensions/NavigationExtensions.kt | 16 +- .../monkey/retromusic/extensions/PaletteEX.kt | 15 +- .../retromusic/extensions/Preference.kt | 14 + .../retromusic/extensions/ViewExtensions.kt | 14 +- .../retromusic/fragments/AlbumCoverStyle.kt | 15 +- .../fragments/CoroutineViewModel.kt | 18 +- .../fragments/DetailListFragment.kt | 16 +- .../retromusic/fragments/LibraryViewModel.kt | 17 +- .../fragments/MiniPlayerFragment.kt | 29 +- .../retromusic/fragments/NowPlayingScreen.kt | 14 + .../retromusic/fragments/VolumeFragment.kt | 21 +- .../fragments/about/AboutFragment.kt | 15 +- .../fragments/albums/AlbumDetailsFragment.kt | 18 +- .../fragments/albums/AlbumDetailsViewModel.kt | 17 +- .../fragments/albums/AlbumsFragment.kt | 16 +- .../artists/ArtistDetailsFragment.kt | 23 +- .../artists/ArtistDetailsViewModel.kt | 16 +- .../fragments/artists/ArtistsFragment.kt | 18 +- .../fragments/base/AbsMainActivityFragment.kt | 14 + .../fragments/base/AbsMusicServiceFragment.kt | 16 +- .../base/AbsPlayerControlsFragment.kt | 15 +- .../fragments/base/AbsPlayerFragment.kt | 17 +- .../AbsRecyclerViewCustomGridSizeFragment.kt | 17 +- .../fragments/base/AbsRecyclerViewFragment.kt | 18 +- .../fragments/folder/FoldersFragment.java | 1340 ++++----- .../fragments/genres/GenreDetailsFragment.kt | 18 +- .../fragments/genres/GenreDetailsViewModel.kt | 16 +- .../fragments/genres/GenresFragment.kt | 13 +- .../retromusic/fragments/home/HomeFragment.kt | 12 +- .../fragments/library/LibraryFragment.kt | 16 +- .../player/PlayerAlbumCoverFragment.kt | 16 +- .../player/adaptive/AdaptiveFragment.kt | 16 +- .../AdaptivePlaybackControlsFragment.kt | 22 +- .../blur/BlurPlaybackControlsFragment.kt | 14 + .../player/blur/BlurPlayerFragment.kt | 16 +- .../fragments/player/card/CardFragment.kt | 14 + .../card/CardPlaybackControlsFragment.kt | 21 +- .../player/cardblur/CardBlurFragment.kt | 15 +- .../CardBlurPlaybackControlsFragment.kt | 14 + .../player/circle/CirclePlayerFragment.kt | 14 +- .../player/classic/ClassicPlayerFragment.kt | 24 +- .../fragments/player/color/ColorFragment.kt | 14 + .../color/ColorPlaybackControlsFragment.kt | 15 +- .../fragments/player/fit/FitFragment.kt | 14 + .../player/fit/FitPlaybackControlsFragment.kt | 17 +- .../flat/FlatPlaybackControlsFragment.kt | 14 + .../player/flat/FlatPlayerFragment.kt | 15 +- .../full/FullPlaybackControlsFragment.kt | 17 +- .../player/full/FullPlayerFragment.kt | 16 +- .../player/gradient/GradientPlayerFragment.kt | 20 +- .../player/home/HomePlayerFragment.kt | 21 +- .../lockscreen/LockScreenControlsFragment.kt | 12 +- .../material/MaterialControlsFragment.kt | 14 + .../player/material/MaterialFragment.kt | 14 + .../fragments/player/normal/PlayerFragment.kt | 19 +- .../normal/PlayerPlaybackControlsFragment.kt | 14 + .../player/peak/PeakPlayerControlFragment.kt | 14 +- .../player/peak/PeakPlayerFragment.kt | 13 +- .../plain/PlainPlaybackControlsFragment.kt | 15 +- .../player/plain/PlainPlayerFragment.kt | 14 + .../simple/SimplePlaybackControlsFragment.kt | 14 + .../player/simple/SimplePlayerFragment.kt | 15 +- .../tiny/TinyPlaybackControlsFragment.kt | 18 +- .../player/tiny/TinyPlayerFragment.kt | 20 +- .../playlists/PlaylistDetailsViewModel.kt | 15 +- .../fragments/playlists/PlaylistsFragment.kt | 14 + .../fragments/queue/PlayingQueueFragment.kt | 11 +- .../fragments/search/SearchFragment.kt | 23 +- .../fragments/settings/AbsSettingsFragment.kt | 10 +- .../fragments/settings/AudioSettings.kt | 11 +- .../settings/ImageSettingFragment.kt | 10 +- .../settings/MainSettingsFragment.kt | 15 +- .../settings/NotificationSettingsFragment.kt | 11 +- .../settings/NowPlayingSettingsFragment.kt | 10 +- .../settings/OtherSettingsFragment.kt | 10 +- .../settings/PersonalizeSettingsFragment.kt | 10 +- .../settings/ThemeSettingsFragment.kt | 11 +- .../fragments/songs/SongsFragment.kt | 15 +- .../retromusic/glide/AlbumGlideRequest.java | 211 +- .../retromusic/glide/ArtistGlideRequest.java | 249 +- .../retromusic/glide/BlurTransformation.kt | 15 +- .../glide/ProfileBannerGlideRequest.java | 111 +- .../glide/RetroMusicColoredTarget.kt | 11 +- .../retromusic/glide/RetroMusicGlideModule.kt | 10 +- .../retromusic/glide/SingleColorTarget.kt | 17 +- .../retromusic/glide/SongGlideRequest.java | 209 +- .../glide/UserProfileGlideRequest.java | 119 +- .../glide/artistimage/ArtistImageLoader.kt | 19 +- .../glide/audiocover/AudioFileCover.java | 12 +- .../audiocover/AudioFileCoverFetcher.java | 89 +- .../audiocover/AudioFileCoverLoader.java | 28 +- .../glide/audiocover/AudioFileCoverUtils.java | 70 +- .../glide/palette/BitmapPaletteResource.java | 38 +- .../glide/palette/BitmapPaletteTarget.java | 15 +- .../palette/BitmapPaletteTranscoder.java | 41 +- .../glide/palette/BitmapPaletteWrapper.java | 25 +- .../helper/HorizontalAdapterHelper.kt | 14 +- .../retromusic/helper/M3UConstants.java | 10 +- .../monkey/retromusic/helper/M3UWriter.kt | 11 +- .../retromusic/helper/MusicPlayerRemote.kt | 17 +- .../helper/MusicProgressViewUpdateHelper.kt | 10 +- .../helper/PlayPauseButtonOnClickHandler.kt | 11 +- .../retromusic/helper/SearchQueryHelper.kt | 12 +- .../monkey/retromusic/helper/ShuffleHelper.kt | 11 +- .../monkey/retromusic/helper/SortOrder.kt | 19 +- .../monkey/retromusic/helper/StackBlur.java | 600 ++-- .../monkey/retromusic/helper/StopWatch.kt | 10 +- .../retromusic/helper/menu/GenreMenuHelper.kt | 10 +- .../helper/menu/PlaylistMenuHelper.kt | 15 +- .../retromusic/helper/menu/SongMenuHelper.kt | 10 +- .../retromusic/helper/menu/SongsMenuHelper.kt | 10 +- .../interfaces/IAlbumClickListener.kt | 16 +- .../interfaces/IArtistClickListener.kt | 16 +- .../retromusic/interfaces/ICabHolder.kt | 11 +- .../retromusic/interfaces/ICallbacks.kt | 16 +- .../IMainActivityFragmentCallbacks.kt | 11 +- .../interfaces/IMusicServiceEventListener.kt | 11 +- .../interfaces/IPaletteColorHolder.kt | 10 +- .../name/monkey/retromusic/lyrics/Lrc.java | 38 +- .../monkey/retromusic/lyrics/LrcEntry.java | 163 +- .../monkey/retromusic/lyrics/LrcHelper.java | 198 +- .../monkey/retromusic/lyrics/LrcUtils.java | 331 ++- .../monkey/retromusic/lyrics/LrcView.java | 1337 ++++----- .../misc/CustomFragmentStatePagerAdapter.java | 383 +-- .../retromusic/misc/DialogAsyncTask.java | 136 +- .../retromusic/misc/GenericFileProvider.java | 3 +- .../monkey/retromusic/misc/LagTracker.java | 95 +- ...teToastMediaScannerCompletionListener.java | 76 +- .../model/lyrics/AbsSynchronizedLyrics.java | 71 +- .../retromusic/model/lyrics/Lyrics.java | 104 +- .../model/lyrics/SynchronizedLyricsLRC.java | 125 +- .../retromusic/network/model/LastFmAlbum.java | 272 +- .../network/model/LastFmArtist.java | 183 +- .../retromusic/network/model/LastFmTrack.java | 290 +- .../retromusic/providers/BlacklistStore.java | 290 +- .../retromusic/providers/HistoryStore.java | 269 +- .../providers/MusicPlaybackQueueStore.java | 337 ++- .../providers/SongPlayCountStore.java | 689 ++--- .../retromusic/repository/SortedCursor.java | 252 +- .../repository/SortedLongCursor.java | 249 +- .../retromusic/service/MultiPlayer.java | 579 ++-- .../retromusic/service/MusicService.java | 2459 +++++++++-------- .../retromusic/service/PlaybackHandler.java | 273 +- .../retromusic/util/ArtistSignatureUtil.java | 50 +- .../util/AutoGeneratedPlaylistBitmap.java | 313 ++- .../monkey/retromusic/util/BitmapEditor.java | 1813 ++++++------ .../monkey/retromusic/util/CalendarUtil.java | 214 +- .../monkey/retromusic/util/ColorUtil.java | 85 +- .../monkey/retromusic/util/Compressor.java | 89 +- .../name/monkey/retromusic/util/FileUtil.java | 393 ++- .../monkey/retromusic/util/ImageUtil.java | 466 ++-- .../monkey/retromusic/util/LyricUtil.java | 177 +- .../retromusic/util/NavigationUtil.java | 133 +- .../monkey/retromusic/util/PlaylistsUtil.java | 527 ++-- .../retromusic/util/RetroColorUtil.java | 345 +-- .../monkey/retromusic/util/RetroUtil.java | 281 +- .../monkey/retromusic/util/RippleUtils.java | 218 +- .../name/monkey/retromusic/util/SAFUtil.java | 479 ++-- .../retromusic/util/SwipeAndDragHelper.java | 90 +- .../monkey/retromusic/util/TempUtils.java | 114 +- .../retromusic/util/color/ImageUtils.java | 211 +- .../color/MediaNotificationProcessor.java | 849 +++--- .../util/color/NotificationColorUtil.java | 1832 ++++++------ .../views/BaselineGridTextView.java | 299 +- .../retromusic/views/BreadCrumbLayout.java | 721 ++--- .../retromusic/views/CircularImageView.java | 548 ++-- .../retromusic/views/ContributorsView.java | 91 +- .../retromusic/views/DrawableGradient.java | 27 +- .../views/HeightFitSquareLayout.java | 46 +- .../views/LollipopFixedWebView.java | 42 +- .../retromusic/views/NetworkImageView.java | 67 +- .../retromusic/views/PopupBackground.java | 227 +- ...ollingViewOnApplyWindowInsetsListener.java | 60 +- .../name/monkey/retromusic/views/SeekArc.java | 953 +++---- .../views/StatusBarMarginFrameLayout.java | 41 +- .../retromusic/views/StatusBarView.java | 58 +- .../retromusic/views/VerticalTextView.java | 74 +- .../volume/AudioVolumeContentObserver.java | 65 +- app/src/main/res/navigation/main_graph.xml | 4 +- build.gradle | 6 +- gradle.properties | 2 +- spotless.gradle | 30 + spotless.license.kt | 14 + 286 files changed, 15604 insertions(+), 13757 deletions(-) create mode 100644 spotless.gradle create mode 100644 spotless.license.kt diff --git a/app/build.gradle b/app/build.gradle index 462a02d1..c1eb4656 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,4 +166,6 @@ dependencies { implementation 'me.jorgecastillo:androidcolorx:0.2.0' implementation 'org.jsoup:jsoup:1.11.1' debugImplementation 'com.amitshekhar.android:debug-db:1.0.6' -} \ No newline at end of file +} + +apply from: '../spotless.gradle' \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/App.kt b/app/src/main/java/code/name/monkey/retromusic/App.kt index de916793..4de597d3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/App.kt +++ b/app/src/main/java/code/name/monkey/retromusic/App.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic import android.widget.Toast diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 55023db2..452b2fa3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2019 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic import android.provider.BaseColumns @@ -47,9 +47,9 @@ object Constants { MediaStore.Audio.AudioColumns.ALBUM_ID, // 7 MediaStore.Audio.AudioColumns.ALBUM, // 8 MediaStore.Audio.AudioColumns.ARTIST_ID, // 9 - MediaStore.Audio.AudioColumns.ARTIST,// 10 - MediaStore.Audio.AudioColumns.COMPOSER,// 11 - "album_artist"//12 + MediaStore.Audio.AudioColumns.ARTIST, // 10 + MediaStore.Audio.AudioColumns.COMPOSER, // 11 + "album_artist" // 12 ) const val NUMBER_OF_TOP_TRACKS = 99 } @@ -135,4 +135,4 @@ const val TOGGLE_SHUFFLE = "toggle_shuffle" const val SONG_GRID_STYLE = "song_grid_style" const val PAUSE_ON_ZERO_VOLUME = "pause_on_zero_volume" const val FILTER_SONG = "filter_song" -const val EXPAND_NOW_PLAYING_PANEL = "expand_now_playing_panel" \ No newline at end of file +const val EXPAND_NOW_PLAYING_PANEL = "expand_now_playing_panel" diff --git a/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt index f5312054..4f1273a0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt +++ b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2020 Hemanth Savarla. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + */ package code.name.monkey.retromusic import androidx.annotation.IntDef @@ -25,4 +39,4 @@ const val GENRES = 6 const val PLAYLISTS = 7 const val HISTORY_PLAYLIST = 8 const val LAST_ADDED_PLAYLIST = 9 -const val TOP_PLAYED_PLAYLIST = 10 \ No newline at end of file +const val TOP_PLAYED_PLAYLIST = 10 diff --git a/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java b/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java index 36f7f565..cbd9204e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java +++ b/app/src/main/java/code/name/monkey/retromusic/LanguageContextWrapper.java @@ -5,39 +5,37 @@ import android.content.ContextWrapper; import android.content.res.Configuration; import android.content.res.Resources; import android.os.LocaleList; - -import java.util.Locale; - import code.name.monkey.appthemehelper.util.VersionUtils; +import java.util.Locale; public class LanguageContextWrapper extends ContextWrapper { - public LanguageContextWrapper(Context base) { - super(base); + public LanguageContextWrapper(Context base) { + super(base); + } + + public static LanguageContextWrapper wrap(Context context, Locale newLocale) { + Resources res = context.getResources(); + Configuration configuration = res.getConfiguration(); + + if (VersionUtils.INSTANCE.hasNougatMR()) { + configuration.setLocale(newLocale); + + LocaleList localeList = new LocaleList(newLocale); + LocaleList.setDefault(localeList); + configuration.setLocales(localeList); + + context = context.createConfigurationContext(configuration); + + } else if (VersionUtils.INSTANCE.hasLollipop()) { + configuration.setLocale(newLocale); + context = context.createConfigurationContext(configuration); + + } else { + configuration.locale = newLocale; + res.updateConfiguration(configuration, res.getDisplayMetrics()); } - public static LanguageContextWrapper wrap(Context context, Locale newLocale) { - Resources res = context.getResources(); - Configuration configuration = res.getConfiguration(); - - if (VersionUtils.INSTANCE.hasNougatMR()) { - configuration.setLocale(newLocale); - - LocaleList localeList = new LocaleList(newLocale); - LocaleList.setDefault(localeList); - configuration.setLocales(localeList); - - context = context.createConfigurationContext(configuration); - - } else if (VersionUtils.INSTANCE.hasLollipop()) { - configuration.setLocale(newLocale); - context = context.createConfigurationContext(configuration); - - } else { - configuration.locale = newLocale; - res.updateConfiguration(configuration, res.getDisplayMetrics()); - } - - return new LanguageContextWrapper(context); - } -} \ No newline at end of file + return new LanguageContextWrapper(context); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java b/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java index abff707a..345cda93 100644 --- a/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java +++ b/app/src/main/java/code/name/monkey/retromusic/RetroBottomSheetBehavior.java @@ -4,36 +4,32 @@ import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; - import androidx.coordinatorlayout.widget.CoordinatorLayout; - import com.google.android.material.bottomsheet.BottomSheetBehavior; - import org.jetbrains.annotations.NotNull; - public class RetroBottomSheetBehavior extends BottomSheetBehavior { - private static final String TAG = "RetroBottomSheetBehavior"; + private static final String TAG = "RetroBottomSheetBehavior"; - private boolean allowDragging = true; + private boolean allowDragging = true; - public RetroBottomSheetBehavior() { + public RetroBottomSheetBehavior() {} + + public RetroBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setAllowDragging(boolean allowDragging) { + this.allowDragging = allowDragging; + } + + @Override + public boolean onInterceptTouchEvent( + @NotNull CoordinatorLayout parent, @NotNull V child, @NotNull MotionEvent event) { + if (!allowDragging) { + return false; } - - public RetroBottomSheetBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void setAllowDragging(boolean allowDragging) { - this.allowDragging = allowDragging; - } - - @Override - public boolean onInterceptTouchEvent(@NotNull CoordinatorLayout parent, @NotNull V child, @NotNull MotionEvent event) { - if (!allowDragging) { - return false; - } - return super.onInterceptTouchEvent(parent, child, event); - } -} \ No newline at end of file + return super.onInterceptTouchEvent(parent, child, event); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt index 13b75956..3f5e7fbc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt @@ -1,17 +1,17 @@ /* - * Copyright (c) 2020 Hemanth Savarala. + * Copyright (c) 2020 Hemanth Savarla. * * Licensed under the GNU General Public License v3 * - * This is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by - * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. + * */ - package code.name.monkey.retromusic.activities import android.animation.ObjectAnimator @@ -234,4 +234,4 @@ class DriveModeActivity : AbsMusicServiceActivity(), Callback { songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong()) songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong()) } -} \ No newline at end of file +} diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java index eea9299c..e66b7f81 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java +++ b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.java @@ -18,82 +18,86 @@ import android.graphics.Color; import android.os.Bundle; import android.view.MenuItem; import android.webkit.WebView; - import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; - -import org.jetbrains.annotations.Nullable; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - import code.name.monkey.appthemehelper.ThemeStore; import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.ColorUtil; import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.activities.base.AbsBaseActivity; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import org.jetbrains.annotations.Nullable; -/** - * Created by hemanths on 2019-09-27. - */ +/** Created by hemanths on 2019-09-27. */ public class LicenseActivity extends AbsBaseActivity { - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - setDrawUnderStatusBar(); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_license); - setStatusbarColorAuto(); - setNavigationbarColorAuto(); - setLightNavigationBar(true); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ToolbarContentTintHelper.colorBackButton(toolbar); - toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); - WebView webView = findViewById(R.id.license); - try { - StringBuilder buf = new StringBuilder(); - InputStream json = getAssets().open("oldindex.html"); - BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); - String str; - while ((str = in.readLine()) != null) { - buf.append(str); - } - in.close(); + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + setDrawUnderStatusBar(); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_license); + setStatusbarColorAuto(); + setNavigationbarColorAuto(); + setLightNavigationBar(true); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ToolbarContentTintHelper.colorBackButton(toolbar); + toolbar.setBackgroundColor(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface)); + WebView webView = findViewById(R.id.license); + try { + StringBuilder buf = new StringBuilder(); + InputStream json = getAssets().open("oldindex.html"); + BufferedReader in = new BufferedReader(new InputStreamReader(json, StandardCharsets.UTF_8)); + String str; + while ((str = in.readLine()) != null) { + buf.append(str); + } + in.close(); - // Inject color values for WebView body background and links - final boolean isDark = ATHUtil.INSTANCE.isWindowBackgroundDark(this); - final String backgroundColor = colorToCSS(ATHUtil.INSTANCE.resolveColor(this, R.attr.colorSurface, - Color.parseColor(isDark ? "#424242" : "#ffffff"))); - final String contentColor = colorToCSS(Color.parseColor(isDark ? "#ffffff" : "#000000")); - final String changeLog = buf.toString() - .replace("{style-placeholder}", - String.format("body { background-color: %s; color: %s; }", backgroundColor, contentColor)) - .replace("{link-color}", colorToCSS(ThemeStore.Companion.accentColor(this))) - .replace("{link-color-active}", - colorToCSS(ColorUtil.INSTANCE.lightenColor(ThemeStore.Companion.accentColor(this)))); - - webView.loadData(changeLog, "text/html", "UTF-8"); - } catch (Throwable e) { - webView.loadData("

Unable to load

" + e.getLocalizedMessage() + "

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

Unable to load

" + e.getLocalizedMessage() + "

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

Unable to load

" + e.getLocalizedMessage() + "

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

Unable to load

" + e.getLocalizedMessage() + "

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

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

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

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

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

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

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

Author of the original algorithm: Mario Klingemann + *

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

- *

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

- *

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

- *

- *

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

- *

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

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

+ * + *

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

+ * + *

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

+ * + *

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

+ * + *

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

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

+ * + *

+ * *

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

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

- *

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

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

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

+ * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

m%D`KhDHN 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 - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1..036d09bc 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1..036d09bc 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index ad35070726c01bd3486444e3c6bead58daa23516..a30acf391ff85645efd88853abef8f593d9740aa 100644 GIT binary patch delta 1824 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 QL;potAa5e*SMNOh2j4P#Bme*a literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index ad35070726c01bd3486444e3c6bead58daa23516..c1a5978fe22de71a13a4f6b5165b4c8e35bc854c 100644 GIT binary patch literal 3821 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 delta 3320 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 delta 2193 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index a5786f83ca909c76986fd3841e82b915e2feb86f..093fff061b5c6569461cc28a66451002ba045c1f 100644 GIT binary patch delta 2385 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..e3cb18319c851d35920250773082c8d65e6c4aec GIT binary patch literal 3257 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 7b5aa2c9c17fb0976eb89b03ab85d2fe0328d6bd..ff9625ba1d32c65067cefa38c19266c692cf5c72 100644 GIT binary patch literal 5414 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 Qi2wiq07*qoM6N<$f<;whg8%>k literal 4613 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 3523604e7c16dab1fbdf0b286c1ee868ecb66bd1..5d629bb1ebabb00b66a064f864b6a5ce1eee6575 100644 GIT binary patch literal 4002 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 W(WTI1qxx&tOZ-0xtrblG literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 3523604e7c16dab1fbdf0b286c1ee868ecb66bd1..710a8ff5deeda48a07fbf4e29e8f0b6ebffed309 100644 GIT binary patch literal 8484 zcmV+SHp`#k5_6?gC6JM-If{%7XQtd5TM&>q@D zduR{sp*>Xf003QLdemAqV(ja5wJoQakRSy5)LOL!1bR-%^J(EyGhmpGmEyG)#O#Q< z5bHv$2eDVF^`L)qp|7!M3=$4lTA)-6npX7zL;h|{%$JxSvE{_}5xYz*f>+vCZexNrSG{+&#|1I6VGki08p*8FsTSM^qY@fV=}Sr#BLJHg*uMs&!0Ej z>NxN-!ia67pN-d`r-e!Rz+u9Z#NRNq3?{aT*j?%|>H?*j9Aau!)Y#*BBgP)=``GhT z*8`S_UdtwWZ59&OtS5up&jrRu`R@s>HwyS0}2ZW$jF{^;9@?r4lk8NuXP~z z3!wE%vWKTu<5EGplvtDmGuZ?@TQS=^MB)=ru9+w?B{fHeTRwChP z#Pr(_N^aE@CT^%Jq4(5J3lhKJa6{EQ#4bvB8`jf+iW=*gxVMG(_-=jepgpA1Ib3bf zhgeV@;5=7haOw(kk|5zd_m$9kT7A?AJX~;GsaxmD8?hQeQ=f??gc7!aYh7)mJ?ii} z_?$H2SL9K^A*V_Mz=XqZ`W}>d4s->aro0%2L9oxOs;WLB z|1RQqo#$Fe8V_q6Z&XxKQ8BKrw$%i^vsUYM`0++!v3MP6QeCyyMVu_lklgr5PHL{z zIxWl8n5@6NygcyHqen(+nd-AbPaqIj)3OR}$dW-wtE{ZNm5`8NqgLXY{Wge1A{-cu zqwrK}JDzu|#D*oBC_X(sy`x%qBK8KzO?4;Fv$XKY4Wg`!j0|s;hKO~c_ApbObn*#H?^8avBrLJ3kwVHY}&NRM5zZGIYa5X+7 za!5$Xgg*nW!PkrS!e`Tdg$4eBWqUT$#9VH!uh-KT90{PX*{M*cxudeG&tE!2LI2uLqdE8h{YNU5;Nyf zTwDx^iHYNtDl=ngCUF)A3ZKcc01Q3J?OXBCquc))21mjWwAXsfh3#8|ATPI2lcznP zfE*zqA)S>%R7>cdYqFjgefCTU@4WdZ*0j3Cz_q0Q)_XV0ENczF1m z*oRlIUe#Ar=+Yx@@s>Pldg5Z!!KeEi-T>m7r^n~8Yx{X9EG*WjP?njQd5`p=f!sqG z8@k9v%$G;#Lc19j2OZl_lLKr++TMXZ>I^NOehpWOiavDxrQ=2lBt`O zmDQe*TtGvY%!|T8u0?~Z!xWhT#M&!u{b0_l?U3{+OA`w9AtxuN@a);MohU?l^3pzf zL^JG`XF0080AkJAZZa%edXTvJe6&b0j1r zoX4>tnT0Xd`xF)y4rB}B<$0ru1gK_y&BhOAP2U1h4^!0&B<#!F+*}~u=Y5%ZA&&Ps zOG8(crU8UzWip1DO4wWU`^L^zpXsFBp)eJ-u zkov#=1q7bB!IYJ#gBHrp&V~yYE)3+|3$dA3Wo6|qMR*@&1c)`eHWT2ZQET98aHL94 zi{J!!X=y1$Mn>|Rk+I`GL}WS2bKIw4fJQZ7iUFyE<5XDo&F_$wmZz%vh#413OG_)- zy?eJi_W9AHM_chV>a15%QnE;qd-Ytu76mR2pHN6f)?+n>UwZpWnTE*MN^XFEo9zSbSGe;S25BKMx5&lfm3*1hEf^eIQ{Uk`5ff z0ZDNXwYB^h2EFwyoDRIHQf0O19I*T~K0f{->2qT`I<4Dc?UR?6*Pq=DRT0aP@!N6r zehN5_*#yReR})(W#)Bkm*a9&Az#mLLoChW&7lO&CMPM@e3+ZgcA}}2|2h2K=5NS4+ zH<0YDC&Kh8zmN{eP$@B;lam8MK|w>X?^`6MsS9gWR#vt_k>Lvgx>QgOZ6ZrR=Rs-R z78?gT>4Bh=eF}7*{RKM03!qba5p>EfHOX{ygFyeUy_w;FzQj2M9??;awB}|(( z&AQoPj16De+1Ud*;R_T8kf>pR(4td8C+#HYWD!r4e-?BE=bH}@*DGZQfx(ua!Q6RL zi@lMx`8XK*?*Bo^^=PQ96f3J2V#b6{pFaITv%?sDlRnA5eo&-%ZW2HdLeNRL2|5|X z3+0>!odV*8ivQ*XB0PuQ{iBRt;v=k~KAJ}K(Y?D#N|*MdRKZ%Iu&}T#$Uu_;;$&Zj zm6esDDgzK&WG?7DCh$BVp6Ds{LM1!^#CoHYQ(*kb60k5I%gbnXw@$NQYr*w4{|jLpvZjCNhZK# z5-*fT1a%P=)I0&iKt#Np$=EM>9Y1Q$#Uv0fbeyF1fh|hw8#29z$H3feGVcSCIA!zI!oBb~B?mXq zR9G0pbpFPT8^0k7QtKd$bz{elMR#RXUS57hWdVv1fX<_TXj*^zMOx2uDAOsu1O|Jy zfO(s7G8xgvb^q(u9EWH53aN%fLX!^%?frZ6bnU{_Def%TDM5oEC%^4w` znVBixyLayZWMb#eod(hhX#KRbv@x8GoIy1KqG^4~xqs98qVv3aA>E9#VEVSdTqbI9 zS@y!Yzrmx#Co)a@sgQn-Li)ss6Vs3hsf08I$S^-Ye=*fR)hZ1TdZE}TDyZo^8Sh%? zA{ee(C)d>?y}akbZ#yqQPELW$+8ljHQBe_Gx^!s`G9eW}^hHJjfncjL2;r6Wr-saY@@3HM8$U&H}pls+iZ|_BL|6USr03id>(a}lv z_V&0{us&s^Uj5UGzQ|rI7H25K7!a=jJtzj9_`96hdVbS-<2m2)rfm=$N88Cwf`_&7 zW7dIypHf<;wj&`SA#2j4NgXH~`i%i(ux;BmH_~bO>IV>pvI07|h-p2)g4%fAikHF< z>K4>2wpJfAVaVS4h2eAj8+sXh4f|TuH1&lkX|0RfOlF%Jz5q%|Nhz2+cdjS0Aq_x= zM~)onLf%`f41CAh$hJVBVL+B*myU~ARwTp+5^OtzlQxQs9QKsrk8sToSmlD8={&DO-qeWfQSdGlnNl~o{SD0 zIN(j-DN}2J&>m(JL4Ass@>jFJ4-)M$l?h8|2n>cN^b#J#OLTFYDHm{9<1>V%rKO3M zELnmf%xLD!nFfslv}4DPZsY_)wamg^Du2D(;__Er;YD!xzYXBkWj1(ud=A|^&yp)F zbs7Hn3=blj&BBToEn3uDYJgU)TGffX`Kd+#6#4Yu^4FTQzG>yJM)85rW$;(bzCo;c zcb(0EglpyH4G&JVUc7LRw=_OEIr-VApMKh-QGnP=#HmxKI#*Owq-z8~O!;eE?b-S! z3ToTk+o6Z&oLT@W5~NQj|4j4`e*=lJv9Vc0h79pQHt2Ll!vGO$TV7rst9DZv4VAyv ztZHpq`Kw{_Y3MTa>pJ0)1PPb$$Qqt^1}ufhh!kF@G9n`*lL!;9ARCmCdJjEUS6AGX z!Me1x^rprD#LSbWy%5xFS}(!0Xky zQo|z&5?`SW&xcitG0~|q**o!{fBwO84r|Ir!vGPHWJ(@eXJ=<`(=32!TL1Vjsmfo? z!v27+?|juPc~+_MAI+4!6B)P1$I0 zWrps81q*N~t5rfm!cz4pX>Fnxii_k3>N?VT;(1K&9RrWyOL&2&F0F*d3<*dW`+xYO zGan9H34i`^O=dfNuqCbdt3H4J`R9+wL{k8=3<(JtuZZ1P6$=oZCu587ah^Pt=} zQtf3u&7CJptX=*}(t5q8=b+v8ZQ$8wfkMD}qt^mhG;cSv8bhATdXkfq%fJ5m>%qvx zlqpk;8wH5w`DT6l_H`iw>aj9pVsZzFDSy4n2x`5wlVBG7JGg$f61;lOl{+{%DCjqS z1N?pdE?-4rO-kV-A|f)qy}fY^$gEedUIvW;ghwkoJ3BM-XjfQRc!>g3rBMJ8)4P`n z7AN+A^UP(??KRXUpUWK{9MjpgLt#IJ$7W!-wD}bW2M1I3cUBua^u~@IYfc_*9zTBk zld`Oh56%;T^Ys$2xXQm-5^Hm%1lpY|0heRN&~s}syuGXd`h0K*x_e9`0(J%g2(zBq zasdZ_A!ywX$Cy2X6tVdfn}Ok$zb8+g+>0zwCZzfa1ij(Iha+v)!NI|kl|e}34c$o; z!3S#v@YV_;@Amd`A-uax07DlGVE7mR+R!;^;N|5Ho(x1Y2}H9<2g?p3-=6c~^O;+j zw7k5$QlagQ0y;A@6E<(&JQrD@Oh^Tg?$Dt_QAk@YT)42OSS)^~=s7XGp?h&6_;^|E z4wi@gXG9=)5_mjWFC>Fd=0h^NF(ZC}v!`z<%g>QBb6_zsF-3lUe*Zxh=pdq0yOimo zShBXZwj|FTi5t77Xd$ggfZm&%3BCHP;)F8(ybzA(41RkV?D_3)$jL5HrY(T0QU6@M zdKI_0YmY2QE1+wHbiaQ6EXbpMY;5dWO$G>U;KXa-*>wi-LemLEtQVTy;70@+0XwaZRn2Fi!+qZ8xDsn}R2Pqn$Ve|9A zcinJ0lvrJm(NW-8?VU)6<{PIk-{OGv)sM)oR?CEF>f(p7cHbnsaN)LX$Qd zXMSL%fB*h%PzOavM{m+}fYAC3-rKCSzMt29omC^-=|L> z+^t@}sQ__xP`jUg`e`W1#wrx~V=Kx4(2&_F(6jfq)C)D9*1z%ELRk9s0Z2*CR+XQ8 zdQO)zSCR~T9QJ*4^iPcr!U{AC3yZcG#&UCW6;@!TEC3Dvq6h|zIS<{5nD+FjKdtZW zGY_Wt{REdU-G}1hGL?Xb?gzwVUstYN!QD7I(!Mups{Y}IF$3zt9D{>{mtb|8BKC|{ z9)L(%KQ9MfC24(qgNuXT`W6oCy9`A|B`O0BcW-DMJlwWz+gj{%UteDY$e=mlt0s(f z@7^61csruReTs^TvK29?s7wH%y*2q3cy*u63@&;RfKUq!=>IkRyzvw~d6KWPKY59d z;o;$VZ@lrw>)7WW9v&9W4PUjwSkIn4ac>xxh=_}ok{`!uzb6-!NKE<)0Ke4a9y}jGDBz)D%#&E+a^60R5@!}!H#l;o!o*z>- zfY1ibh=PyC{S4=VZZRuBmHmh0q!G`Nk&%_tr%xY+eeLGvX4Ty6>!mP;dnR`1(7}#8 zy2ZuCotC#3QYL^t_)-AVz865iMFBj?E&?1gQE_-`X5Iq>1F?ACjrO%kOT$+!F-^1( zifN}+t5%H=3WZ{Mru51J(AcE{SbsVc(}jJ6BQLzW* z?pp!u2oyp_PAQa^s~tEs-Uol;`@n$%n0b+0$sK=G#f&fUTxUv832on-9O>!lPpB)e zRu_QA5S_E}h!B!85S$k}N8RAzE?W)_4bAcK@xjfb+B-Nn*zl3MH<;2Rp_J5)0RaI^ zQN7b&1eYm54HM&|13YL8jwOXv6XDlifBhNzj`pD??NdD+IUKAPV)hZTva-T0@VkYDgIDlH%*MVVx(B_Fl{|!VW1$Qu-P$@iIk}@-Jbd`@kGLfW{jJsKvVtf(zY?|_ zFNH}fg!Rr_t==z$h{W3SRvHElJJbF=K0Y2-nf{7>MEj~OXTnrhM?ptTD8m9IOG`_< zFwb-6&K=Fo&6W8mwWz!bQnM@I(j5_OJ6Qtj50}8^qa_e{trTJ(i$G9Zp|Rj_Lsx2Q zD(v6C{|xpG?W1;z3|;kwG7D2vQwO>f#EZn*t61HSHz2RZs>?JD8cBFC8XP@(G?erS z?(^Kq#KZ)rqAU~{y6W;mdUOc9H91@`#uD?napT549I(QHD^2c9MGY2W#&zn{sfVO5 z@TZzQXy3HvOjqdYD9ORqL)Lb7c5XOI?e6a0=hm%TiTKmeT8QKePeDNeZjPDk=H}KP z`-1j~wKC|Q#`F-5XxdPr*0o>1egg?aDXb^bcCvinxp3h^TJPSy-@!g0eSxyVhW3rV zGQv}jI0Nuuzu8J8{X69Z+2M{=rzSq?<$@7A$n z$9|VDU%rR+ClsRR+K%ThRmV2PHC-o8oQUkup~HVDJe_F9WiHRD3O*r1wQuL>=-8P& zb~i9Ecr_>}C^$Vmof#*>p(3p=YMIxegM`mhQc@rwAmBP_8J}^)=(JXCd zW=LSr&K_8M<9>ZD`y3(~u3sLh!7M2yq#>T?Feft7Q+xEmZrfqF6k*=!@ zJoQ0jM+dRGk{j)@W5S8d$5aWl3lwk>UAJ5`6LE{HG= z+Zr1icd)j$?oOV2&!0bkdPqn}I*wXng`75|NgcAPQA=LDcrkOTM>fA{X)5*$56QHb9(tv9MIlyMj& zG&Hni{rdHru+6aT=oitMwyCK)!c*5^BaVjR?q?1b78ae`v}xmoZg}L#ksqEueL6HD zAptPHQ;}G$sS}|H4z>Y$X7oBF`3M^}Y}hDlD{M1tJ8VPRmL{}q^>ozy;8rwRQo-kH zXJ?0Q7(wc}Zr!@?u3x{Nf$2V*sbO1cO6A}SZeCagFZRiF~6x%*@OfZ$#qyfA;U+|NEUgcZAv5+3XHlVs#B|9FHT$7> zty{N*J9q9p?BwJ$0NVuHhO|)^Y%AJktXS44c`Xj#Noq-&)mO% zzZ^3U3Q`pdlB(~F!3|Xi4r;)A_wJP+Jb3W0Aw!0Yti$Wr)3!2EcfHe8Z)8NrE-;gD zGBPsi+^$_aZ%p^G5hqTZI3XY);2aU!rRdgC7of0I#qh=rQ<#!tn11r)36ANM9y)aB z{P^+X{U|uz*ap}Z)a%&LHZsywuOqEDvZC?5J-Teth&V{r0~-}ZemlY``Ow`*vr{0FCQN}@3oPxl6*|lreq4(ZCv<=KOS?e^{8|hP5ZB7fJaQToc^+;@J z^C8x2^yty!w`|$6^Zfbqkr5FQBFx^eZZc5|81#IxNx%H^%htfaz#Cy_Cr#uf+wF_+H%I3%yHnax$*F65waxXW?fqSg>GT_wL;XQ%{4}f&UbWl97w#z5MN*4Vg38}AC894@o)Hl z6cAqwmz?+4z(R$vH~macyav1$dQDtt*f3>4??nra0zi`VN?e%Q()8DXe&Z3SPBfci zpdvu{V7kolR$TLuuv-65y-E*y4h9xHH+}{Mrak>^J9;f#Xe7Oj791so$&?PfwWi|K zmI_rz>JeOMXQLMBh@Fof13{qHjapXq(mII0NV;z_WzCPdki_CaADB`qajd9!lfqx8rSN2jpTVW?)ZQ81oG3Y$wg6jXTpgnWS-C$30>v zsinBBTTdrVC$8H%?!-=0r;gpaaq7fPY-i;7mgCs6Ygk$&ne)zxt`@Xk}j11@DJe-H~a30RXd8F_o6C#TuJ5KQDWje#QJRv~{awxKw z2tuaH`#Er>88CceV?MX4s;Z=^sj0TUzTVf+&=6R$Vnr|fIeG;>h!-K=hPVuI1tJW=D8lr2m+3h#(rX^3 z_qdtfa~MA(22j}!OsNA+7AeV5Ns_(_llwTt8v+z&1%Ae3@VQ^X&&FdQ6>?y**@LI2 z$OBUgz_JbEc9_H;l47V$au_D}3PlKmR0d-WjunDE#r{fUnp z6~LT_g=&)n6SxT`=?;jqDig>jIV1pLmio*JaPiMnJ_nES4*0i?iZ6hpN+v&|1{V!2 zOu|ht8BZ%GQJB103sP2Gs5nq6$MZCv1D=b@m&AFP0Smw8p`C=<_)dZvX?j3I_H8 z6}6d7Rd6)YoKlq@fG^FM!fZSKD9$4SP9r=hRmB?QvwH!i zxKydAl2WcEp+h`>}oBQ*NkzQoTGO7+Jbcx)2wgpY1YqU&1p8U8e;4;8^l<3nynn2kD-y%YpLI<6zvdg zbFrayE;jlhTF$Ud7|jq(XV_I3jS#*wY%|0buXb$m-pPW}HYRzzhBv5z3cX!bRn=w} zQ!Vkft3`YDKm5C=kR!~PK4I2mG__CZX9*j3{pws}v8ZP^X6 zE5x?$46$ujhuHQV5Zgm+$F>l=dTWU7+&mY%Hig*kjUl#Y1H}3e+Y2$iF2p9*gyq<` zI?N_l&BgwaFgq|DW(S8LR)*Q3L5P7cJKPV^7iQBvVK&npW=FapI>YQ}5TYZ@js?Q( zcsoQ}n4R#4*-4D%Fgw*0X4m>+$06U(nBOyKRw{c;fG4SbD2L$rj9I>?rDG?+b7dYL zhz)80?bQL$mc#+tKN=0tlpcUO6ackG19YtpfKK`@vvz5{8R<-h2r|a4idN#}m}WSj z5i@;{AJUfy4_6-GMVxT!d0MDWRXn>BW0n~15 zfWnl@-l?vxZq1m#A(tiI2UFJ>E80>0fjtp$;=v=TPa^4S&OnD303@r=Xmkap2GC3o z8E6rpKr}!n?FW#U%I?jWzx|&j#(7v!9a9$!TLB;rOaz>94R{ujzC;6!A;Y6#pp^hk zs{s^T2+#>7K#leQh|e7W6t>DxXOa5cXH_#i0we=7J`av=c!cUJ0??+pP?qme>Z1do zh=CRX>eK;HldS-Xs1N}3V8;ASLQUQ4+Gyogg$$4gjcAxv;Nht&A@x}o+yBS_iRz0qQd0<4vi387qg~LUtk8=HI zO1T29OkbVq=_}C+EEx(!UIyZwql9NE>Z3P(i2y~?mlB{x z+X5udDP)-o)z#I0Bm0JO$ou?WGVaxb0Ey5*zQ7Z6YB zRG%aPB+o6H>6)6Fn@Kp9+%uHP^C7=)kJa}iM1VwLybn;hCRv?^ZSCVxu!tdoqZ^)hsY}cIsF>%t`(Le!9w7Sz z6fx6~#V;1O4ysq;`%ed4VBEOznGCIB&EO=!k9M1*xk)8Xhh6(K648rY{=+B&RXR zK6hly|CrvQs;@kFqeqibea!=OV8P}omhil%Cjw0j)zN|{uJ2ij_wg@Yee42IZEfxU z00XYYq%nEl3mc7u`!y%!IvEYeObwwFxlhns10C695ngk^$ z5&wh4<-;3%H|YR%98`x#*m>W%Wv(T40XSOl4C{wyS`8i*<7`CbWk-N&YHEH!K1lby zsAd0CQc_X{Z}4)mQ&>AFke;>;ti~qR#s-I{EYb7mh9{BKwaCiLZYd1^1S5UU<#KsA z7kbkd`kw*e_AIl>OkuHFH^eQr8l_kBtC|w7OPQ zRGcDV$arO?vXO=q zb`LV47a)`>7rgL(TL9EHrP(m11rJ|XoLC!@6-X5vQFXKoa~$P4VG=xc0|+WHY_>1d zTgu~;3gLzSZHE-r(SAs^eM4;rUTyasO|^Va9|A`Wo*Bg|Gxt226_-iA$BrqCDzT@4 ziINN*zm&*#UflrDGqwPz?NHo?v3RW;XsS_xR8=Qdsji|t$1Q-zh5(%fCaQURbG#Og zoT8$lIsoVuy8sl``?G0reLs_h@92i7#XNXy2++BLf`TSJJM(g_Us_t) z3@?1k4gdw)4jb0@gQgl4Xhy^DBnpqwzQ?Wr)z{a*U0ht;rDsb+-ZZPMtPFqxy>Ewg zAQ>P%eLpeaQC_O7+*KFB;fsWto<~$3li;z_IuPy3e*mH~s2ia2^73wY;bnUOG_46v z)FnDP;NbwFA3_DXMzOlg&2qBgu^T|3Kp&Qrl&qpMJ}2HHVt#dXbsxO&*LDD?697_! zCXwDB)t1$Ph1ar2z==_9ow5KP8v=9%L}j&}wIB{qzg++dwjNf6M+7Hs$DvlsYX6)9 zDd9OGZjPEIGWga>2B_Y!OMjM_zkE>jB2kGG*J15{B_5d;(co*k+%f73d)Yt8w{E$Cu_qt$U3 zuYF^>=L$Q}`aGL#`4O9J{t?@c(KH_i8X`a%+zKy#(+&VhQWF~p{vq45=3*2$@l+?#-k*J4FS4%2{mkd_Cq2BL7C3jw4!^O% z0Xkn)RMe{nAh{FK<#I`OXeOJ7sF{s)-NyEgy{Fl2C^pG2rg02oZ}2G=Y~1bqIE%_c zVt#&pI{_58jK?Qc0zf~r3qTlNfM_WA88$I?Ssk1Ndw;YxQYVAYvku=@2cCsn8TG_Q zcD%hXyemTm0Q9(>0GflSg$;FlnvIXVyAT`$;Nc64lY!@0mv6!Wh~~bgrsjWXFRUQm zURWkQsl1_~;h*f#&Y|XsMmla`6C>|ODNe$TV;b=Ag~fesPqUzJ*K!Al_XA-&M}=O1 zu0jel@S}6 z@c_b0I7;z16zrD578vXHo7uj>cNTynUK|*=>riVyj20FT_@80j-u-r|ijsA4;h!KA zi2&q=7ag|yPglz6to?KBz~CkQU55$4!&jvbwmi!^y;m>GpUT9$bd;8sUWZKR1qhvX zNkv7)m|ZqhW2ZA88)&_O9qhjt4UUH2i3N}9ei*Gv&osZzx~1{uq(qU8RJYr`jXDww z^ZO!EhiGNXjcls>f+{#i;x&(H!K2uTKjM28lxQm5 zA(C(TTUl9oH?lxXWa$M6X{!MFI+|*~0gvfy)c;3ps`Grb*U=9!WKT<{)ZfLrpibLmh9$Pgz=b*J#q$ zx6?1pq#HzG@;`WsJ1?=2n~}h6?y{+DtAocasR&Rcoi%@oO$XkNfTMb)g<9Kc0u5x- z-m~P1(rW>+qN3t1ET#ycgl=({MU_}pd3pJ6;SQ~{sRxmlt!%uG9cg=4TXhWRc|PVl z59ur|R;+m&Ysl?(&m?M~l|BgfKN>;Kew?Z*#Fc2c@ndY-|Hd3RiYqO6+v-vq=4X5_ zvH|Is3~d^uuV-QITZw`EMDNnd7Zl6U2Z5QoJ(U6CN;KN|33k+fK|5s`aJ9vZ_oY-{ zY~<p1sRf#qZ@;%@ca2h~6t+hLchG?o?;8}J@yN1NY?F3}CXOy`*I9Wd5MQc;O! zsrve;w6t^z=YBE%NAy8*cOs@SpfYIsnqD{c0zph?gVM=V zgNUauycQuJOIj`lRVpN@rnQJcNvM3#G5hM9X)zG=>hmCO0>?4^-%Nt08LltY^m|CGd;w zc6h(Znmk>`?BYu&*iqlFOoiyE?@dsm6RDs?eDUyCFxMlLzDkz*vzI)LVLz-7ROstg z|L{tev}?f?n_Hy20isSr(%Fb~11QBgb1xDEV?8LHvbz$6$UJ`o=b8Xobox?OV`!ur z+-~=F$dQ(s=`Lxv>Se#%y?;;FPc0oHoZMr+w@ify-+w?lYF8zSnCIf+;(a*Rw0)72 zvBaOejeL#JFnUj;Vn$)Mth|)do6&plq3y^yg=1;o-G&doa*#__J zkj~l-`TUh&cyU|8Y$Y0zuD3d!g+T!Dny=5(wuPzcqx3?!#VDeZ*84J~Ryro8`W$i5j5+|!!&yMhGV3L4y_<=L4I?a52yh?=}qd8 zu7dPsVymQ2fMNzsgNJ^)=^V_}Ix@};Rv71ahPsreGLXd+;D^Gg-u{}d z*SSOL3cKCz-=fsjM01yCRqE18WiDD9!iA_H-b)4-r)ZUP)|ot8>~+ne?eLl0mDSLJbpb^GK&Cq6Oknc&y6_8KSy}ndI5#v$HOWj}TB)praw=X5(hU%O z&w&SeRWBb7EKZy|G!j&_bmfvOm3RPS`fw>r@ zIq?bCd1cu_eboF=1uaemTrSrtaQ#1bAW9lM&%+$7!MUJ0sj!3ZiRXt3X;Th!ZAWo& z@kW5?7Y;<$!UJReQc+RSX4ioy;wwz_;O`N5IyI3B5Rt%KzYs*JiOwn zPUIGKb}-es{Bj_oKL}E~8a)w}_#vmnH@sAr4Ks}Kj|&P4))G9eGzZ02ZdDiwQ3*9@ zw?TB~=H?DSIr;CxI+3&3Xv`wvEbVW7AI5kkjx~*WEzLo$-AuFI%8Zxyb#y{Oyg(5k z`X-*R)z_G%W6Gk-ad4gIJ{a46Ql2&%bFo}c8F;iHs^!KSPP7-kEnmVb0jz4yPuIJs z;uw~dmHh>dDH&$JpgbuHj~Yb9WagSkVtYzTO0I>R{j$&s;-oZvDJx8V=T#_Bt|NFl zsZ_6~vCc~+&!g896;V0qr4#PPbqbK*?_x^BZik5qYEVG6zTYIyfev)Q{n{>y)y(Y3v$kEL}9V zSs7{m5IY*(WcV8C+k_zX!#C{HU=ClQa(_9l8_n{?fW6)DVp4yCL2%FD~|MXw{MPC&SRotx!l z1&^99a?vhvokETJAgzsKFAGTchYCdsr(3Gx;NZD}C;V49_X!o%siU!SrMurrRE_d! z?V^glVVK{0FmMpvZuc|{>bK!rc@aS(k2IUY>1|g0BH%gUxq%0K5YF`ofzw0LMq^S% zW0b#KRY$KHl~R89l4PTN_u`|-CjoC%MAbzd4PpmLxvWTdJvaD+&%xk70ft*tB{3x=8>m^uvB<}W$BM^{ z$Bvg4!?~;x6sMcc)ko)EPGgbhs7_*PltWToOpQ?WQ+z##D-H*zzyZg2kP2vU%c;-BL;F8Lg(~3$4dmzNZv~*P$6uWRkMTX2 z%(DR1yBsR&l2n6B&$a@in@U7r!S}{yF8nO~O!({@;25@%()26TCP>Hbp>rvB6vsM{ z@@U!KO_fPK8SWMVCaz2opddyI3k%0#Vt*GD;fpZQe*+W!08IMhQ2qQA0DA#{WESt0 z3~?U*`yCAYZ~Qy_H*C+v=hAB)!1ur#bK`sBd*cpm~_t^6s zaI6I>n<^5nN~Ho!67Bl8)A~t|pioMv21p@>aPkWZ3O0gjYzG*2LhPp4iGPRxMt}@s zx)fe7!qOu|SH}ERpppU_o~Cl?TpVzc0FqLbM3^e6{B59bJWgUORXANd-3cW#S1eJv z#EATx;x&B&EWClei9WM|j-gTkjZ$qKaBK=wAuYU>kvXj3=!AFzbOsS=n6r+P8 zNWuTVou1P|uW6$9kmx<@=rbzlGfU_g9B5J$rfizDTr#IFT2m<||HQ$nq|{PPhPIYq z5Tlm2^HxYP?3_57}^0rK#*>P8Gi`UFhdCf z(#_DV@Q&|a-(TN)?^^qwwa(h{oco-6_TDE(Pe+xKjD-vUK&h_w*Z@DG{|V_e{M%sK zg9HHjLiNXrMgddXx!28%oG!ZF8sDIin${Ndhe$|Rwqtw!1n&#_6g@7)T8SaHls51A zOkj6D{xPUKyttp5A8DyZo)H^)|DI5(T4X`LNJqucRVdZ!8XPJh)*s=cFyU)MQLsqi zb2V6-bMjTSSecWCl!zb#`hOaVNN!ACmm!rQlTpk{|F}i`P%r})R4Yxysj(aVLa(mu zd6)3YP&Sz_{eG0V;4yT>{*V4&MH+8(p40JPLnWK*D&*F5y>z|wy%FO4ms_=t!jJoA z13urmGHQM4e0+L6zs*Q#x;>N?TcSaF+)LXTSUQz?HU7miLS@#fJz(){VTdc*S2M7` zT|`PlM`v)_okTJD{p$UL6YkkL&%GztBQ;$moEfL-0C*! z&y2P)Hes&924<-S$8*ZB?{N#g)#9PO%fo}Coqh41tIBX2^V?Ni-a&Fkae&&qfOk$b zx}cx{=9Jjk)g@a>1#b&J5d~Du&CO+c=g@Sz;FvxnAqiEvOJWbq(E6-npwbWqoBgnl z;Q&wuLQXNUlm-0r=Ep|W1OXx8wz2Rk01 zk1h1{l7f1X+S!sa2N;{LXxKuhGa=~4j&R}#2lM2z*>2tXb0^rdGnT&Ih!7eOAdO;L z{d;kuaJzB``6p!k4u|}s60E&&bukeLT%xU4veF#)H%#a@J$CcK)4CVcR07q?* zE@XXvGgweWq$}XgDq>2R4fGP>R9RwteLCl(-%|nl; z>fA=G#t7iK1bEB=HUJnyfR`xnb0}NdC{q zvZ1o-A(h4b{T@s=!Kf98@6sZJ9gw%l?uxn3Od~eN!*Zvewh6UUeUcOmaoa@>xh+!b z0|*!u0Nu#Pb8@WCPkOgUyB%CJ`zuTG zHfdO7Pc>yj)&%xe2|p6R-ASM@SUX?faZ>RQ%VkkiGw!KmmUBo`ln_iS!W;rUBH+aB znr{sIdAu2@?Cx+P_1%%pi!@Og9>``E5d?Y)U~*DM+^gR-O%m12(t=VSl0SN|O{&0` zr@**E3^x82XlL0v10>5pMnl)<6=7M)4&vZZ~?`XE*gwO=DtXV1|1%}x{tc4`II(a*3etf)E#x3AV4Uaq^c~$Cv z5<}N=M%~-n8!-uiH-IKXhCqrmhN;fSAHJX821q$TSMP??9O0?_Bvzba8}e@^wP@mN z`NAf>CBK9O8=h8A2(qMollAcBZ}Ym)%`nl-8vNui1BhiAW4j02w3iK9igdmp1V}s3 zkFXy4>qvOlMtx+uh78zJf+)fCfukt}HEYh7oBQ%#Y%a)|-jsD#1*6(1*%=|NU zvrvboNgwyyTXOr|qkFYkcbt)tL zjdNGHxvTAiAoKGxYZ{p0^3PQN@>B6=+}36J4u-&X?`g!rTD@G%Qq@XQrak$6>8M*u zmzq?Tm)ca8D6Y4pdjS&`FMiT$QK8E3xB3yMICM8I)!8$TU2-OEJbIWsJ(Y0#WGdap zWKZuzy>9-ZO~Nbm2B;zcl{pXI3k@2z=yWFI4EWaHhu@y5|5B)#$eH_`cU`Mh4bj}H zRMX0?c+9KR^SmRx+o}8Q(zr)cC&GBQ{fU0EYEhp&1b`B0po&%V*I|-Zhdw)U!4`RP z=h~?j0lqsOIxl`UCcBe_D0vA!G4u17dMU2Waz&Z@=y~;&R4+iAfGV9Fo*5MgI*mge z!NOAm4JfKb%ZcJ{zir#>SuW~X!d*tGDDG~Y3JF?%ePd8DO9TLt0DRw6M>HW`B9zrm zPSPU6DN=)P=ofx&}?#- zzbB^KBkI|1Pc~uBl`K_=ch<4dde*nIZ30B-QXH~6~d+)XlXrHDD*iuEj>C0F8 z)lhf*@v5Anm_nHc&$QEUkHc+pkA^j~I-{6XUyH`r=}n4;4KMmojteylzc1r6_W;}i zMCW!Ws@Gc=I1H9sjRBA#K*p=2`6^+}M)9G$g|IiN=x) zzCW3@yeOkg&c<;e7rgD6oWdXHe6Gb>NF5ZJkG$fSxXT7yZ~k+*LXBTX$|DA27W+PB z+i-!xx!9U}BB6^x9ghhgy|)NRC`f;&$zT&KqUT8X)H<627*T>ml@xV;gOH16Iq|GXYIf@WO7bfOFZdXIsqrr=l8P?~yx2#FWLzk%PKx z@Rw(fj`MxIf;q%n*xTLPis3zItFrsruI6JcldkT+_SrUu#}j2|6--)qke+>wJ8%QL z#kla_4V}NRU+g}tOJ!p#HX-<>H~E}7MvrwBP%(ym`qcLOK4P7I@wYH@xPWgE8f8`; z$l-cKRqv)ROQYd8e}hlKrFGolPDbkL`iPGxTZ{rV5WoHpO5LO^SM68<0~~#ZzS8d# zx^$8Z%UzFB^ z$LlCDGx6UEhxQd*B-Bkc_$e81WdkNkf7%@M$y1dmUBbB1#QZf`ZAu;aE9k1z^avpE z$f#4R)0t{li{&2$$8HZxW)4P5H7~%%v{#PRr}#7tdduuttD#3d+EozX$^zU3g1S6R zo4=%aMbB`DbMEBP7G*o~?c)wBfPKzqknzBivI_~IUeQI>u4`?ry=jaDBVH}u8-J8m zq!_&;N*swTV}*<2FaHwhyoP{->b3O9KHmEe7<^->xhmyTG>G8KBa;oByH^i4Kh~R( zK|pLdt$|-!*_yllp1-B;Wn_zQD9Q-nB6rz+w`ug2IlrErhqS!pr;nQlP)xNe+4`V& z*{)d~4!6_u!$m2x-pvjY;*NwOygu>vrlp_!p*!6OpV>X z4=E&i553e*B)11pHwR31;ltL=S?gsD=D4Y16}?~82lt#V3-)nFXXV4qxv0ZiJMpk5 zKN4Z+yU74qj?m4cHAi|NbocvFWBtTV6&Lrjn3+fisXSceQRrnYJ7P73MN|j3>7{=N z`6sp;)y54fWT1o}d}mejUuj%0jmuJ;OS-8e=px9;v9o6)7OwIyb02yRQ)y_58Lh7p z19vjeTk(RiVcIh~&N0Hmr9m(IMBXs%jAL6ajbQZ;G$zKh@Hd-e7Xe6PjmwJ_{Ayt& z{@qW?KS}jec4K#6t1Mk^Szq@9wd-_7B_GduVW-OlfYt)O)?QBpi+$^o;8IOEcYW3+ zao|y2N!G_pB;*5DHy=}|D`oFv1#AOlx^g)YGi9Y`qs^mdnCk^xl$;-gb|yy;XH-_! zPfjT;S^~)b{ym}eNW_<-OfRR@j;ndf`V_nOkTE&O+1+=i9_KxbALW;R#hx9W+oG_? z@4GD>^;P!W-JQWjBBDo5)s#fkCs|5{ycM5le+VyJW3~Xcy+hZ6Tr-cxTuLL@Ep3s! z3KGNQVu#|-rmW?*gR1~MA#7=;=Qoe}oZ`-P@vV2nbj*Q|>-`gmfC;djWl`o5o6=~z z5Ue$4n5$li+BtcTDZvGk5W-m~&^|kPcfEz>nI%@d7}ow&`1a-IrMO`b`7dW;`xx;_ zg0GgHwJN#A&p?A~(^P1)E)diUCrHIXVCIXTL*bvp%CG+m8{{|5zcC&fOBB&KN`sJRSY9T_C*A0FSap3b zf}aooTl&A7X~V&TG>i!1rs*@+4gZ%oj~DO6<-sr?5Y|yEV5c77e?Y#gAAY#odAQn6 z6z2sc26aSe{|#dJ(bDeklCm|zCPqGKVZ9{cYmXS!EW_ftQutfDSUG?z6CA-)Y_QWF zw*z)hI}F`Au^;%CH{Q7`jQ^`=w1T>JNRKuiB!AMPD8Q3Eno<3)TDU4P{j}+Z#b(z9 z?=WLG#rUM8Onp?t08$aaF7g$r0U@9SyHgZ$160_NZz)eiot)KGLzy_hXaH!MW`1a+`|zrtQ~8NA*J8&X zfPC_SlvUJH_h|=ZYPwg!)|N-=LjYw3&zfs@NeKU@#mjPlpEyR150!l-`_(J)#zC^r zL|?`8jGh5>7bri+m^rObn<0z0G0drcVh1|Sul^a(!J|VnO{6ZPp>m;(15iXo79Y8< zJkU!SV(1chCV(n9=90oDfXBMFR!PVRGK5@?pc#c?wY`JA6sM zZ{zWwjg0tYg3f?(8w&1gg^*qjcB=vo9iE-WK?x+-qOWOJQyMYV`RWY z$4mYVZlKWAB{jTUX4O)TO;NuKj9%(Vu|8Gt{ME4i z-0o5U-wc~I{xrOP{rcp=>QH!0+se+4jj9AVJwG33u(2l2g#`u%nr4{b^=^XpYuu8O zlAS|CJ9gJ>yOX$t+#%@prxO)}6!4_t9!FUse8&$<;Yzq{jDNzdqN0*Z$Nuyi46k4< zc${|+(7f0hDb}^V#wjBc$W~ltNgP3aqlOfiR#@c^4-bnRXKYER8bgLr;o;O_&+Y8& zxX*&jWyq(y-jW&{G@ij(I5>3483Z!W?3)vnHgXaWv~Pw(3XgUZk^7y|mH*z5SASJa z2-s_GdQJ0r38Nq-A(8ey;^_76IN{a|= zON^+SNH1_;Xn81K`K{~G##r`G!!5yMreJMtz!wU1BR3@7M@yPdgHTBmCO3+8KcwSc z|5qCSo5s%suJJ2Ex#G;HInnW;(z?};F*rtUtjbO~4~(U?Iaf-8oF#o%ZVa^uV0#lH z>G88_Z+E^UH77?P?XgzFZ7P|EP?oft&h5xD(@N|P>Pn{|mFfojMi=b8+?d*NMv4W? zTyQTZSKc)V`b^}j)2Ys$o>EI&+d1)Miklg(LUGuUk)qtOjF8t2uPBFn+rQZ&H4#FX zZ?8z)4ps(=od{wYuPMXe*G@yR1zI!uqU(zgX1EihE6Gs5_Gek;<*Dt(|uJ49T6epin3qC=@NNmVNUE zn%!2oiuWh$){SGb;HdwFwEy=B|^)c1e9l|M5DEbp$BYoe- z8ThIbW;c|PiK^J*SCz=N!S7a5qUokx#}dlKn%A;1Qftr z-DCK!lGHI#u=24Q>!jGb*o@z>Z9w-1ZpiSVesR*%A&|@PX?}y~{2%9s&@7YSNpepo z@4BQm1Dj)=FB^`R1vJv#VwQ}}QDwr01Kw4cT7l$hBkn@M`PEzR&TCAq9CxIRqz(|N zlsU^rEB8{YP1&E0i5Z5f(TZO6%Q*@+^~{zC26tA=vZDBiR+zu(vpdQ3t$#`X^NDL) z{p?S!ldd#_%z|m~0fH#9jhTkV5f{Va@}~w0akKpYU5ZBc{A_+p+ zD6wJLuh04Y@%v-SIWzanoqL}<&$;(Z(qjWn(t8Z|001D>)>1RR8WPN z`I|MGeH)w&*0ftPTOK?PKN+~(!aa0*Bln%1Ja|HtJDg44^I*Vz zm6OSn`@Zd-PD?^YMsu%B|DxP3p}~l4l0D5M^t_8rvxxv4^(&5;R&r>8V1>BjiBYyF zrt=}%my_Wpm;{#=@t#yMrj!e5!;t`wlUv;JpWOY$a&d9t0xac@jg4IZ>o>rrz|a7} zU-_nM)K&zcVkto={tm5&yxTEvwHE^`6ZKdz#;{7=iR_v-j#9W3@ zZuV{lW@ad}RJavwAAlrSnjAC5E)o6ifF$JSGg6WF;;5T{{*R0>^7}W($>+=sqbIRj zLYtR)ni23;{HGO(N!-A*`)fRa;AKp|3mBsnk0^nPa&X5YPy7@kG_ef@-$Pd3PH+ag z7#tzhU`VtbFqY=8#M;a&TlD;M!ATj{NZOy?UAlcWe7>|-mOrQC2nT777_+q)f`fCN zT&bB$#+#9%3u?gKYGnb{=f7|1qV=E4yO6~wu+X<e3&3$iHE#KnknH?w$?!~-6g>jN5LsljIbEtU zInk3Ay-Y-USALl@86J@lQZ8b6mx#ng+#6>Ye{y0q!8u~b$};xR!O$HcEFW=INy+n( z>d{{@24}m&+VN<7;%HRdt%=EUl2(~(C8e?x#K?J-D!4u#1X}z`G60&sLj^iDV)9{&4ZISDXP^swk`mXoF=@383?+Z{;|HtxVT8UbQ)wAMpBf z*iKz}xnp;cggBq?Fk5}hP)HPkw+@)s&EvHe8vLkBzeP`m3i~`wI(Fi>h}Jk4#59KBQgPwq3-pcV|`%7 z3=I0+5){^>#$&%J#zVf@INu(ANXvf04On}H)k0FlZ z6||LO!IN3E&gbJ7uce0ur}Mk7)x6;SuPd0A&XXnxh4nXNV~r3|)ID+*nNP`X*)gvg zT?cB#k>DW%8mRxF_2V{&dqNH#my zO4dmv9|Ms9q{1?YKN&!$srs^J0FdDD+Qik(?4#o&bs4` zrKAx}M70nl^f)In{yPx@Wmo`wJK#iaqn=4NgmD-CLxM&#KX$svXk1NKS;X9OF>cx$ z6AtxbaaZ6j9Bi?$YSWN=h*+#`7aSE2*6fDEFH4|NQ4c<{UsNu8lTT^ z{=i5nN5S9U|H9{U9GuSx} z;Kz8vP!41i2q&Uu7&%u%x0Rhs*umH&{%QXmw?;d1pB#VUWQO!KTm9vLx^cJDS#I z`FbQZ8&81MZ}GNR3&9RYK!ZT%t|+NYh4oKq%fAe#uWc4^sz0w3w_)xtc8Horpk+@} zUPWp03kcM8O{AJT_>B<_F+_ER=i+&-X7?5ROd$m=Ezf%#U z?_&*?*dqar(j*arVm3Y0qQLLXBHAF916P(LM->NPd(AV(c6pxN8&8YK9WJEnm3<+= z!yCz@1BlW-QX=bS`|OVcJ8at;JVDJ8HD%!05n?J882{_e31;4Y-Zv} zGHxZodrA*{$hn5;3u#g7k(NQC{-ZA;mi`_zs9n(315DPR+v>^2`~Rrk-~Eng`hr&av5B-P0VG6z6UPhFGA_DK=mAO2ARgXNcmsnoO)6KXN?26O(#85WwkxQ_oE`Cw< z`1NPmK0Du20*lv+pK{Y8Y;t6KkXw%!@0`oZi6FwHRvPN`4-$clw{L8ffD)2HkeGW> zg_2`W<`%eXEunazw-Dy;qPioTklXXYI|$?^5`u<6Mx$Nj5g5d`$yE%_(8v9xNclR}Pe$wka0+V|8UeMI_UK zX8$3w^vA{T=*TL3G@~{vvFF^i`1hM{8UXU3b$NNYzb0GxKP)KRzr^P6B{^h$?e&5U zsYS$gok$2S2$?1jJdJGB4@n?Frag@0JlF+JPB_5mV$Qd7d$KBTWDhpk28fJ|oSb%- zy@I>dQvhy(L7!K8-o^qf=w0r<=sl?y0j+}JJMHrOrV(WC=dXJcf#z2qwoV4$AnnsV z5_r8u_@mILoKh2|bRZ3{)ZU-8Q|~so{Sd5khlNp*aiW+Rzrls*z=do3O#G7*F)&&V zrGWGZ=C|fVkKnhrw|jam;P^q=yr4S4pmD41-!d#)ONZlp{@nG~p(IG}p=(y{=F31v z5Sr&F$jrD(n8nYTDZp`BL(#IpO!+zEYHcRT!PUfO94DU~Uwfhy$+ zJx6*k9aHf9nW?-Cqz9R%Jmq*|Vsgd@j&!UD2JGFezz|G<&G$JH@S4??R+e*D!h1?| zyPYB*T38w{VuVPT`CI9;lM@y2hh%4^j$K)N9SlRRGZYm!y7S(P3t@&H?~t`(e4_zy zqHLC~bCHeOA;MzwH?E63Ir56#Du7;W6hMxWWyoSj6eyuej(mm(yHbH#^DWn&jhHXL zgzHFV)`z6utgbF!yS8YGf4OgQd06*X$KuQ9-wd7)9`NT3vH7Q0R-gHNG<*V7g~6M z&wmVrtNXI~_b6(s3W5@%Ts3iXf5`zsGL?}SoC`^DA&YqOH}8IZE7v^$1;waI@Uxy*Hw%}A}!beCsbqW0M^L+ zfOBBfKx*NB|3JnjZLPfd|9njDMu+v{RtgWl@dl0f52rK4*wP?Y8l>a%jGh7&ECq9*hNi!c zJE;~xnybA-)~Q)=2B#o}b7hL}MMzNr@OfRk#=!d*keoGkgf;l$`lgHct3U%BInUhD zfBU8xQ(}L@MN5Yd%avPyo;HF!aRp0ANc5STnLSjldVCG}P*d}Av~Y`x3vv90oa!&l zK%rN#iU6`nELLn{`mpdvS8oo%;){#eq+ zyWPgnC~wmp3H-=f@u8Df;JzPc|0FqclOQWlC6n=z_4Rcl%vrW9k$}*D+=%X zYb8-1AYvC+8X;Tds6c6ufhtR=2F+FfPO4ZN1sRcXP1bGpNYiFBQ6MB^M}zV2Bhd&fd_tnJed;oi~l>+9a7?o zC*V zWRgOj00bpHv_5cB7#b#yB46A3x6M(U;}9xa>T**)0;@at#(I93=s4UW1fIV97uJ=# zxH>n`A~w zS&lx#LuJz47De(a=o$Xp!yu{p37#SPjvpwn*0f0o3iC5 zW1?ZaBTExt5F}i}6cO+Ha$bbTay6WAL)=G)7L&hzvt5z9w|0u`tzy!d`MW;|J|cjQ z#_a}Cj& z!X^lMVqvJ7|Ni^$MuNyZW8ymc;}-{3IF&AHH0=l8|KR1i%WR+Ac;V?04x2kWtS6AA z3(L}C|nyPm-0+i>pcV|35LnFMd zV}x6CJ`q+jwYG;6Im~P@E6AWCMykX5UwXFcp9`EwyE2a{XpwuE<<|!jw!W{qX^_NEI%e~xMOVgw=Wq)t{g+g|gs>vwwmyt`p_+7e@|d3rpWS^cj%msGBl<*(YKEwkJa1tT4=V!-aRsoEmn}6w?>QLue;wu*g?lHl8!Vr4=vcIIgs=4s}dNZ*7H!zE3}I{D4W?sp=7y!h8$w=_U_a8nKs#yO7~1ovrZ{^A`*ocMtSEjA}-$-WLr5_mZ<>(_oZi1v>e`8tscQ0En!YZG7#JP*84o_k&(&Pa}05Ql^ni`|HQj=0`OIA-5wg7sy?=Q)M z(R?_M{3iTO? zsd5iZ@DT^Ig~Iz;hdrgOIUM+JPuvQjp-lgvo!Ap?TJJbsAL*}4Z6@oL^;B#+(chG! z16<73f6T|eO6{4K-|!AEf`)C@llpzGym>!#9hvAFffxI+jcc&lJH4E7KblwMAe+}A z4KUhZ4oVk#Zk_e>`ne*1jApHQ)duFD;C!?@a#SvGy4_$SIuSKbC3NWVIFu z>Xo}2iia&B_x+8XCLZh59WW1_q-sBRI8s})u;6=;!o~`qzVxfNAw~#`*ylmih*%8p z_1v_h^6~TkUAqA}XQ}yFYyE`w2gU4~Yp+b3K{wg=$9bREi{F2mn%dId9E@u+&n5lM z>TOpRMYnEdV{?51k9(qr;N?w+9IQ<6CTQdsKGGnQb|?X=3?Wr%b9>j4PhayZlIAyD zZv;1Yq&;u_9Tyje=W~|k?BvCk13|@4k$&D9V>S+q82P0a5Sn1;*#C)4a|t_?aZB0M z#w4oFX!q}E03%~!gt&w*Q<~6bP|M^$6*$S2!taag%jSzQjHnzCQGSZEp19IpORIC}uELFJ72>(%I~TeYc290fU=Y9{;si zR~gTQ1{hZsvNKJ^HvIsqFuPu8?rL`BFvD6ox9UFD86#qrbNpt`lzUXU=MPuC-S zrRQD!@B%6VK!6hz9%xM(#rS*NhS zlLy}4$*utvm{D0C|Fbw=XZ6M~y?w!1gBAYq()pbjd?o{x_IUAH964rOOw$OxO6XG_k1iX6fN=T|h{+04qwDH{(FwuJMwI4)H!6VzBW<*ZN9U zZu^U(Zg0v_hBA~tX5xij>%Fb$FwmTA|#sTgSRhUuyCG|wc% zhoz64Lo8wAwMt+s=1=v{$q8|-U(Y_bdUFe+CmV*J;0;>2KpBe)CW!@yiU2AnD~_2N zV;%%fbP=nI$`@uw3s1zoHf;H^K*NyEWyySceKS~1zxzzu>FCOnWci{Gt;ag1PlcwZ zz2n4%PY;^ET4f)iRt3^>7r+S`3=|Jx*}1t5cXh#h>UC~sJCsU)+T0u0pvZf3esrZm z^n`}64F|nUBK~HZAAO=Hc^YbiCb_MtdIfAz>!x?%Z;}enTtKqhmFlug zd^2u^t>zXMI03a7sW{`B6oM}oSCs#q z3?|8TW^C?FddbR)=w$YQIzM8_Je&wY&Bx0L&BMS z?S6nJ&tw}{uO-7IA=pi6w*c>k`>sJ;7|%BS?8he5$Drk8>+=sZ5{L&If?FSuF&e4#MOEia6nY&~qv}NNpb#-#! z?Mr2Lsq_}!)K`S#Z~Jaly9^Z-@RUyip_2`1CAQ6w$Q?L^fSOq)S2z3@WyO%#e5pMo z?YzkKR1e1f5Sga`+>w?i<)WP~3cqoSQ>!E-#QbTMX0+m@7~dQ*KSX;;&^&3LDp;=M zcUJ>-#n8){olxyqi2MLs;G3AQ-Be>3rlG^j1#x#8xM5t`rArkLX|S^Nawg-4;dzao z*3yb0Aok}7Ma2m|haS(^Wj&FhhG{q*_>Y8Nc{9XCt=vn&(`fPrPm(S zt$Hf4Uv_WQ*Z@+L0&2VKC?2dT44l`G zd?OD1J+}Rkwg-8K*CHuHtapjjz-y-p(5*2;ZvewooJ2?8@H-)I9kiP?gS(4SaXM=o zahCf{5fmREe}v+`0+bD-@L*Y$1FGFhNB>nL@!3qjxbjBr6zq@eNFa{lP1K!6j$*a8 z^Z)#1UTKDeCz!u}{efif{%H(1JBQzbDhtJFEGTWf^?t=Lk21m6kx2yi1iV>gM?yvK z#c}FE6w4rE*tnrnwAC=@5dm@FN#73QFqMq8ldbd@4Ij&ako&McG#L27sVegr=mVRcRaI4c;Pk+l-|QQyxOiN` zV9cQIjO!HSn?f00fL-@Nsjq8p?%(O?R!S3c1-e0qv&tkSuu6b!IR|u zugre+<~;%F$5lo^80j}({BS~{{i@Eq1Q#)CB56DxNE$^%=6zDQC=HAb4>C;=)uzry z)ESVPgj>}qeOB!YIViek>jbiGeRCWY8blVYe>|BKIkz3vZp=FH5QGtYU>IdfzGH8J4f65#@YKs<*3{Ph3? z0u#?(4mP0WtBLY85J=R?@UPns118p{Io~~8{GhRE#N0FhaetLtn?DBOVN;D=i1uXd zyfVE=kd8|T@8VB?FI+%2%>3e7@jk>>!1(&N_nOR}wr>n?#Ku16nT>od|j;^O**1z3FPO&TDFOG_WsV140ecF}e(}~+iw?gM^CAoh{?63Xy?jN9P!wVArYpa{iW}!-QpXpDZ z|E>!@bO5c$G&)~)_&1;`6J|BDOIOhe1`YJ_1QxOuM+plfGvun2XTV>aSX-YuvQ{lz z*J)HAjoEVRE00KzL^*?4%=}elrNPz3>RrL|>mnc7XuBY<;Q?fOSXY973O_Q#`T29Z z-p<>}6lw~~SC!7Fs&3gEjnxnB)+F<_CyF?e@63xUpOZxPPCiJzFmmdU)*gF&^bqWI zv9&qp5-{yZS9dcvR8fBaH+ky^lboK1xnXUgz_~7@ zu<;=c@hxcr!&48eGADXup;E1H;c^A(b7KM^FlGQ7s5^74LJMt4k^||7T_VhbF>g=O z?^%GuKt@mKB_DJ5$vqLn?gTbK_)CxuS9x)w8yJ|fh)MoUclBQz<4N&x zm)934XOg$77jq(|9{sw;1bj?koINi%6t`AC1Vr=po_R8cpIKZu)2d0**MWTXs+PB@ zg#hsVt4zU>Rn3C+_BAp;+u7#&G5jA*d{e=@_rvI)I`#k5csF~`ocG<*s2IhWc|{kM z&V(;ePOnWp=P>E*YV?pSB$Qc$>wzNKJ8)N9FP}a={?jFx$@oQKam_t0=OUK2F|^|C zFn3O(v9!<9(zWsEYi^HzN8Eo_i2KhcxLmsHxxw{(HfD=ztYetXYgaW3j8Ww&&PT^D z;ettSA8rwqeZ!2-<@SL6o~93IqeQW8xrGoFyQ(UC#CI;2X3Z=Aq}JX5S1YvA)LF+q zAtIbAp@b7-HEH9j!#&vT%U}F*ZQ>(VnYyb%J`%_+y|<_xG;P;*`*e9+8Fi~-uYUPd z*<<~nz0ts(b7yH2w>uCXAm zR2`q5blbJ)xN{zd9xoARb6PG4WFL$Z1Ae?CR<5qfNK9^Ll;@`>)!heca}sBHT?ANm zG@`8uthn}BQbYS}sU9S<(Enwd=+Dp=l&KKDR*4&Si;vAkf-BOOH>NGxpdD%N-K0}G zitX3ojYdh*OO+Y6%_30ExosKNiDR>5x|5livqzeo{xOJJ(*YJ3^)`)abMbDU-CN=d zR^=*lG<((L9V&!JD|3(Bm99pwWB90@AQ!=BMjMc0GOgrA>o<$&LgojC9OWdlWNYe} zKv*^h@rc@i++sGC7Vv-KF(u6AF)hy4Q=sU&NY`nCP(N>HH(iu}cK8H-{qRIaY;yaF zA$sjgYnehAh}s8=r~K};+oY_1hGY!YLneLX%G?Ao($XVisZDhK1%*)heyx639GovQx6Q@^x1f$YP5RpjYr|Sf`n%$~Y*XKDBsGbW zM*TSZ#yTCL97MuvQp?G|>1DKF)gXNHkfn}>JibBEP$#I-%o47n#qj`Ty$-BKD5F_q z)DwG|#Sy}|{#{VK98C-I6s2Nz##KTR_W>ow7XwbTdVb)jVm zhFoQVc5PJ}m)FNIyoVF3}G=VxGN`!$<<}=Bxi`*r*ftJ?R6NrdZd>pzAy95S9?Uh6ygEK*4rJq zD#3@$N+6Z;L|UvrSA~W8n+sNNhdV%hT+>ag_VwX6>lME(C()geKUiA++pp470&2r0n_15#TVtDrs(;>OuS}jK)Nj0^pV^dKIMyjx!RzB*^ zytUmoiDgHlSDld(-bUN~`0|G!@Q5aiVKN*w{qqQpitxC!MBc5!a}t+m!G^bcdkYJl z*4@8>>co+rw>0$VaMa{9<-8EGjS|x_|A(Lx;#E}PQB_Y5*Mq&=|BjLe!Co4zOQn#= z&Vwqw!*Wl&E`&`Ry;-sfA@-(PY7(V<^B0+aJq!DL?}w52YmzK=wkJ`YuAtZ=6Msiu zx4Lkj@cE*syD+#;5b4>p+wxckmcx)_V0zC2++E)x_qHQP%=u&lcz-yOJ zaNkeX8vge3xwKn+7;bGP!9V2*g@sX7IexB?WF5fM^T|~bxTHI-f$8CiEYa|}cT}%S zt?5|hz2m|BSOZH-eRr0zh(`dFvwj~K-amw+$T?|mS@9B$$&$gYNXD>G5AzVfrM33EywK0Wpeyz=RJ(U_UoooCViy=5o1K0YymUC50}^&|1p-N=4U& zMq;hA*GP={W4mipj;)X9+%GM^Y)H!%T{rV{*L*e`m{7H>xa@YiaTuC}MlEJHSih)p zD-K)bBzBe2^r|ISo~gld8p9LYA%x!g*0_#q(1Iqv2C1pwfxqW;gV9)s+V5Bto3ZxT z1D5iOtpa#X0O{^}Fv%GBpWT!lkCS__mm`EuCxhwXBON~YbC=G6Ho+{U7SlIgVr@1` z5y1NK^l26gSp?7HuXIaRto%4?TG-OfWnrK#BbntlIvdXM+jw@X`6Ssu2$(9L zF$qE5svF@B>~Jfk6YQRDET=ebW2f&bJ zb1tKSD3eGk%@D<4GE>Xi)QDI3p)-3YSt3>nE-SO~y3YfdzCnSFgkJB}H;7l1=WHN# zgJFJtchUZGr|u&&k-DiWb0n)B@m?kwJ-|9VfERoum!KX0?4mUAAv2mUh_Tig0%T?YRCAYg@X9{9M0xstpJ zfZ~JT+gmV6cKFugteN(B=1b5V%~$it3I9^V=?BMu%~g-OFKaZTPYxW~y$No`%=^s& ziI=zlP3FsJX%dDN2-J1J{_>&9ZZRaEJ= zot$6%t!Hl?uo7-Q@(5<4Prr3#%HS(V5JV&mWb?&zh;-wltN{;*Vt zqz*)Ws>J=lJ`%;grlU3GvC_v=ak%{Yj@oIUG+dxnb)-kdasUU3Af|>V{-CzAp%-m~_tobHOk+9@vZluVUVHY! zccze}7QN7RY&~&!Abw-@Z5JX`ftqWVBKqG+?Nqz*;wr_*ta>hd%?4$(C~^ysN71<# zBfBXFwTCo!-jdpaSmpwq`Vwve+47VAGI+mRh82Dj#_;kMR4+0X9BI?kqxD*o0!ECI-tR}Q-@+#dpvkW=0RRJTIVfyk4NnHwo1O#LV9w;6PpN) zWu9Zw5|t}g@bgZOw|uf&uixeUbuUZ-&d*jlMS4<0)0y0WwVe03wEtK`Rtk*utb!8L>xZWCFH_+JXJU zyg>i_gREgogduob9JwBkJ2(8nahSNvD?1TN*1px|DO`|r~$bJf&fZMyfOUtPJUNg)o)$wS(Fl03HdIoU@GO$ zBu`jo45M=kNO05!x6kTU!G^C^qb2)zC2@Y%3Q}I*kWp$JCdNf%b!neXZ2-&ykepHi zX}M|VU%2P$Yam4*x$XB@k!5Ufn@)*_r|Yk?!mavsYs>)v(7Jl3MQjvYe&r2`v>0Iu z=c-PVnW>58^5IiF#3Rx6*~OU2BvOs|%1XE*ezvZm{Y{MZPkV6nrLUi!B9h&1`W(LF z#KtntbgGwS5>+`xhEf`Y=8z&* zc#mCDz=`fq3*W8z+^cd0eOp)+dkPH^4HP^&Fh<%9X7T9;1gfR*AZAha#_I0oJ^x^p zp}U&}id!40F;@H2Smw*T)h9qA@f%K`P=<$i01?oAqD1$!eR~r^=&uC^#FrJTHsEWR zaRA3LPAT7M1_jWqix_brW2?#z%u6Yw5qN!eA2a@`C8yecKis@m(_SB76M}n53x~v|%3?3n?vO zMTp(>tnI9kT#&kFu~n+(_i%AYgmGWCw9fCLBi+}&YnX>P3@A+sq9g!Wcm)tk*4t;+ zC*1e8q*FVdqAuauaT(M|N*rwxi~R(x_yWi(*zdKh1>n)5{aT^u1xffrJ84@rH zV?kI6wSyA(+aL9@T~P!mC_2k%oj^*FscsFYh9}N_8!DzS>v&$hbtUK2Ma^}tN7Me8YtenRpehMaT0 zoSHKHHFxWkVjdODZ)l({)7W*O%F+U)V6$nf$AKhw47XJ4^*jN3^A_yM&x18yz~)Prj_-FAIpqxQLveSl zL}22Uqe4$I04fmUAqryUsHQc4fxy)%xXsvvx#VN@myc=)yh+FN^_#mNTtulW73)b% ztMgY{t6z?N?c(bhS9IN5*``I$brcE0CG`Kt-Ey`MJ_-z>V_k;i%PH%mtCmZYYdAC$|7 z{lz+V80|0Mx0@7FJ-Q{x$Sa2Ed~_33zhqEP{N19B$DY3E0d5sI@f}IWqf4L$87|^C zt)EekEQXbe37Kg39beGdZGr5l5>&2-sx7?o6rV9C$I!ks1Ru_Ce?JZzmS)MD33KSwGl`Z^L1RdKFhTXEJhff4o8*tR@-5qoGQuqWO$Zn247kg;@Uj5?utoQ* z>vR$kJ750SYf^W2=0l77Esf>d(III!q*@;a{1UDJDkO(tI6at@x<}StCH{`I`W^-{ z&!AS~EMAggdY}D_avap}-VKa-WFonEj*aC#$OvJtbmDk7fs{n}9P0Uyv>O|~Y!wDN zk3@*#7+>FLd|GSAPWsy25_y6v$Zgzzb&+t*YaBq58}@wB%^@M1HFoWwJO~aS+EU+m z8dGrBy1J%^Uq8&vAGP{xqx#5er|^~@%vN`8Yeh|O={hSP(wg9DAW2#_itLPM3*i_qd^#(~ zNu)jYS{a}(Y1lIbJvnVXmSbQ+O7gu5=q6FB(j8J*2WfBs@KJNuVq`QyJot`YnsjR- z%h-Kzb&2nwlR#8zjLif|an+;XJGEu*{doZE=9Gnn(6P3(3?1FNy88Un(>E8XyuRZ= zsh0&?C=F2+)AFKKvyD|0^b{rd*8$D1KNpxExGA*4?)v&VI(}D?1%TAiMbV?x`8Y*2 z*;=E={c+gKyzb~Ew?+uys5o7f-?0kjuM+NvrQL$5&Chof98`Mj1k1GoVdUw^TsK+) zO6*LHLDQyIn&e-R`}wxrjhAo zS6lGU*6Uvi0(e~2uCiNdJEySUFqP}|7z>P3*nJo`%S)Z|gmoh`}?Ezhibvff=p-@2H^l16~&hdB;<6CTAU zHhBH~A~jZYmEIM7pdU7yYn0jr^Ii zKBf2UmcWLVpgtQroNNUpZt-lK(BAjtoOI{@)=HaiU^d(ivR)||%Tk1sv zpOhzfM0S+Pwf%=0)g=9BNpN55D0+I3|hjD zl9{vEUw0a?yJ#vY>2s24^a3oS+`4U&QMVLYv8y#I+HKq9@O`#TmY*FVIlyNFGK3I6 z-#OZl5ANGr|Hk8iYNG6hii*`*b0k+uWBkA4`E9@E+ncQ4%3m*g6TX%MDsO@4l+E=H zrL@q8C`Da1Wh<4f2X!DX*)Mm|er<&FA|odXdHr>m9G_mQ#hS|e?%;pj1W1o4nMi5- zBJO>Cr^F-a9({QR@HUW^C=zAj9MGVyQUh@!y!Ze0jw~+WI_a%A$C}(Lt^R3Uhi>Wy zr~er(>AgMgY0(q{+uhM#VhIzl9>Y5>JN$y5;MH)Z^|ZaviO|*52A1kjCMn}riwQYZ zlsRsi?kIcF5BPoo1+t+f_2LI7D9hK*pf~6_c}QeLz9(!g2OaD~PcdNYng@@K@aZ(q zB`oBxI~Bbq<*2)ms5!MkTOW}<-ir2HAlKkmS%buo`RA&m&7&?g5YZ7=&7<@}jcTo> zkD;?#f}ly!C$fDZ!66sqU4jm^EHvGhwzU90%thQ;eIIS{y;}qSfvnnJ2h-K^qp8Yr zD1w4yzU=4h#|bLpUs(yR0^`dHaL(!qX;i zbBm%2fGwJSeH8YvL42rp)M#VXFJ&LswV<2++~V=bQgI*ojxf!tqm5q~(EW$}yd=;g zqyu}j@U+o;$8%I(H;_S><*(*c0ETTu!@bp_O&5o4--)4m-^MKvEJId3D{S<9SJjLX zX#pMi+j-}kRdcoQw5tCG++}&3BM@F$b??9lEM?E+=EC}a<`2<8AA{YF41ctEIm_;B PQ-*(={6+W^8S(!BxI0z2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index a57669d6581321ec62e14476a520095a247284e8..8ed2b6c2c34343fdf4af34c8647e477605bb9b15 100644 GIT binary patch literal 12231 zcmYLPWmFpt(@jDM?(SNwxVw8PP@s6x;%>zuxEFYEDM5-Ar%-6IP=dRZ;#!=R;0|Bj zU*DdS-Tc^{-E&6n+&hz?tF4BIO^poz0Pr-_mGw|p`2T*G5Y$<3#T5ep;QOedtY8qZ z^!FWxF{#mS2w0hrh$xFKp2HYUJ?daH+`z`BO5$Qui)C}KrDdZb)TC^iu$$C{FfLUP zz~&M?PK45d-zcldb84Dd9e6$k$4W`o;xsqfLHS!Ei++qR z*EyzMmgFBlG@IG@7_NiANvvERbDSB8nE~joH3tapT;czWJzw6eCWD?o{)CeJKL2Gi zmGcVn!eSHOO-Tqn6&`6vbQls^gsGyUVmmZ>gnRRrJ@Tiz$hRH#pQ84s5#qt2wUrXp zfCpyC0~bk&dM}IB@|FFXJnaSrXg&L5*e(t|`PB{XkTd@w(G3giTWejjLoYth^_}v+ zWR{*m4gl=(CO<>^$^hyV?vS;7V|~fT=YgG}{0l6{<6_(&6nYSd7NU)g zj$RWQ5^~5-Yyf-^%&R4GQkIM1CP2#*u8n60rYpsjmX=Zr5my1iwq)Od%W)TSxw|+O zG!Xu#x3{-qMY5rXBkH;dy*yi@9cjN)bJrTEOqlO*BYd3&5)u+t7m7{LohE+%{Q1n% z0DFU^;2+kv-HHThMmh`+51%v&JDDJ9rh4m`{`iTag^#nL-nSSH?gMM!KQ67E+uok3 z`6_T^A5^1LF>hgcC(u&IOm@!)fxg@lJuvVif6Eg8K^+5wg9MeV{Iyvd?D;(~PiRs^ z&xdrz0$Kq9fs;8dV&q27=+39%BUZ@M(ksvdXU6b9`(7Z~UwP^2)*9{)`3vw=0!6kArJu6~0ax5;-ahA4>aFULdy}yB&VbWW zzZC9b67I?p9rqik@p~OrRnchhdX#1OX~Z?;3HQ^gLuAL1Fw#*7E$G7|wKV%9F!=d9 zfPjJ36KoCrmCQsQ&jm#w5K3ScKT>{{M_+b=Fz>G~!K1-B`}J*nvGK@3e)~CWPe)Pu zu_JuG-_-dZqwDFqp|6ipi3rli0X5%=wrz+h6kT198RE&%kLTij_gFW76#Q^Lt{tN_ zK0WHOnhkJ`fg9O-_&yMK`op4*kIf;bkFlXj-a&k6LO-+ftcny+GV{&zucR!A; z_X}50V5+tB0dpUw)C%6j%&*vkLc|;}q!Yp?;5EA!j$vW4(t&5JV}pa0L~^Fw{el>w zT%n?gO#3o)o+PFVI(j&$w^fOOmRj?%pUrkKhKCPu;m)iIhKA(Q0Z1#U7trp{TfiT; zk+Rlj90L1K51k-N(f0|ay?iaImufyHpGgMmy#$jYjAc#%>588BSAP>S!K!-y$=+30 zS6}^M+qzk^%)tvfumLZaq|Q@1mIkFe!z;xM;e#TJUd%g!J0(K+e*MJrXTVHSgsn5-6$2&s0$IKK41D_B; z`PAe6^)=1_#)S&(l4-`lH_wg4Zy`P}&zn-fx7rZiARac=b#AoLwMQ$qF#veC^N8b_ zNvWu$#GWAe67dR#3gZg!`5T)w+47Vepoec^xgQ7cqEDXOiOVRfhCkmBVzidqVnK(J=mVRhWQvm}^{!IBwYZY|TyPK!(zNbQKbKXXy zqN4J={+nChLpfu-nqunRqVO+k$oKExVF^vw zIoLo=v>l?2`57ETUMT>Uj(%}_Jg~cPXJJ_Ab!1vv8lRKAYwhgX|2%m)9J4y-S6(@X zg>0_1A$_ofo%B9?Vv5D~%0D}+Vs^QC_}(l@90>aYqV- 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 literal 10135 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 - #1C1C1C + #FFFFFF \ No newline at end of file