;
-}
+# OkHttp
+-keepattributes Signature
+-keepattributes *Annotation*
+-keep interface com.squareup.okhttp3.** { *; }
+-dontwarn com.squareup.okhttp3.**
--keep class !android.support.v7.internal.view.menu.**,** {*;}
+#-dontwarn
+#-ignorewarnings
--dontwarn
--ignorewarnings
+#Jaudiotagger
+-dontwarn org.jaudiotagger.**
+-dontwarn org.jcodec.**
+-keep class org.jaudiotagger.** { *; }
+-keep class org.jcodec.** { *; }
-# ------- FastScrollRecycleView START -------
--keep class com.simplecityapps.recyclerview_fastscroll.views.FastScrollPopup { *; }
-# ------- FastScrollRecycleView END -------
-
--keep public class android.support.design.widget.BottomNavigationView { *; }
--keep public class android.support.design.internal.BottomNavigationMenuView { *; }
--keep public class android.support.design.internal.BottomNavigationPresenter { *; }
--keep public class android.support.design.internal.BottomNavigationItemView { *; }
-
-#-dontwarn android.support.v8.renderscript.*
-#-keepclassmembers class android.support.v8.renderscript.RenderScript {
-# native *** rsn*(...);
-# native *** n*(...);
-#}
-
-#-keep class org.jaudiotagger.** { *; }
-
-#For cast
--keep class code.name.monkey.retromusic.cast.CastOptionsProvider { *; }
--keep class android.support.** { *; }
--keep class com.google.** { *; }
--keep class java.nio.file.** { *; }
\ No newline at end of file
+-keepclassmembers enum * { *; }
+-keepattributes *Annotation*, Signature, Exception
+-keepnames class androidx.navigation.fragment.NavHostFragment
+-keep class * extends androidx.fragment.app.Fragment{}
+-keepnames class * extends android.os.Parcelable
+-keepnames class * extends java.io.Serializable
+-keep class code.name.monkey.retromusic.network.model.** { *; }
+-keep class code.name.monkey.retromusic.model.** { *; }
+-keep class com.google.android.material.bottomsheet.** { *; }
\ No newline at end of file
diff --git a/app/src/.DS_Store b/app/src/.DS_Store
deleted file mode 100644
index c1698b6fe..000000000
Binary files a/app/src/.DS_Store and /dev/null differ
diff --git a/app/src/debug/res/font/bold.ttf b/app/src/debug/res/font/bold.ttf
new file mode 100644
index 000000000..96619df92
Binary files /dev/null and b/app/src/debug/res/font/bold.ttf differ
diff --git a/app/src/main/res/font/sans_bold.ttf b/app/src/debug/res/font/google_sans_bold.ttf
old mode 100755
new mode 100644
similarity index 100%
rename from app/src/main/res/font/sans_bold.ttf
rename to app/src/debug/res/font/google_sans_bold.ttf
diff --git a/app/src/debug/res/font/google_sans_medium.ttf b/app/src/debug/res/font/google_sans_medium.ttf
new file mode 100644
index 000000000..1543660da
Binary files /dev/null and b/app/src/debug/res/font/google_sans_medium.ttf differ
diff --git a/app/src/main/res/font/sans_regular.ttf b/app/src/debug/res/font/google_sans_regular.ttf
old mode 100755
new mode 100644
similarity index 100%
rename from app/src/main/res/font/sans_regular.ttf
rename to app/src/debug/res/font/google_sans_regular.ttf
diff --git a/app/src/debug/res/font/medium.ttf b/app/src/debug/res/font/medium.ttf
new file mode 100644
index 000000000..fd818d6f5
Binary files /dev/null and b/app/src/debug/res/font/medium.ttf differ
diff --git a/app/src/debug/res/font/regular.ttf b/app/src/debug/res/font/regular.ttf
new file mode 100644
index 000000000..e2c69c3fb
Binary files /dev/null and b/app/src/debug/res/font/regular.ttf differ
diff --git a/app/src/main/res/font/font.xml b/app/src/debug/res/font/sans.xml
similarity index 53%
rename from app/src/main/res/font/font.xml
rename to app/src/debug/res/font/sans.xml
index 4fe9ca2da..7bbc8513b 100644
--- a/app/src/main/res/font/font.xml
+++ b/app/src/debug/res/font/sans.xml
@@ -1,11 +1,12 @@
+
\ No newline at end of file
diff --git a/app/src/debug/res/values/bools.xml b/app/src/debug/res/values/bools.xml
new file mode 100644
index 000000000..7d3f0ee62
--- /dev/null
+++ b/app/src/debug/res/values/bools.xml
@@ -0,0 +1,5 @@
+
+
+ true
+ false
+
\ No newline at end of file
diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml
new file mode 100644
index 000000000..79e559838
--- /dev/null
+++ b/app/src/debug/res/values/donottranslate.xml
@@ -0,0 +1,4 @@
+
+
+ Metro Debug
+
\ No newline at end of file
diff --git a/app/src/debug/res/values/styles.xml b/app/src/debug/res/values/styles.xml
new file mode 100644
index 000000000..f3761b219
--- /dev/null
+++ b/app/src/debug/res/values/styles.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/.DS_Store b/app/src/main/.DS_Store
deleted file mode 100644
index d1bb56ec9..000000000
Binary files a/app/src/main/.DS_Store and /dev/null differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9c70b148..14fbaaab7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,30 +1,57 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="code.name.monkey.retromusic"
+ android:installLocation="auto">
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+ android:theme="@style/Theme.RetroMusic.FollowSystem"
+ android:usesCleartextTraffic="true"
+ tools:ignore="UnusedAttribute">
+ android:name=".activities.MainActivity"
+ android:exported="true"
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.RetroMusic.SplashScreen">
@@ -35,7 +62,6 @@
-
@@ -72,7 +98,7 @@
-
+
@@ -95,89 +121,71 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+ android:name=".activities.LockScreenActivity"
+ android:excludeFromRecents="true"
+ android:launchMode="singleTask"
+ android:showOnLockScreen="true" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+ android:theme="@android:style/Theme.Translucent.NoTitleBar" />
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/.DS_Store b/app/src/main/assets/.DS_Store
deleted file mode 100644
index 5c7365722..000000000
Binary files a/app/src/main/assets/.DS_Store and /dev/null differ
diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json
deleted file mode 100644
index ad828efd3..000000000
--- a/app/src/main/assets/contributors.json
+++ /dev/null
@@ -1,38 +0,0 @@
-[
- {
- "name": "Lennart Glamann",
- "summary": "Design contributor",
- "link": "https://t.me/FlixbusLennart",
- "profile_image": "https://i.imgur.com/Q5Nsx1R.jpg"
- },
- {
- "name": "Vitor Monteriro",
- "summary": "Telegram contributor",
- "link": "https://t.me/mluizvitor",
- "profile_image": "https://i.imgur.com/vvpE4nn.jpg"
- },
- {
- "name": "Elvan Putra",
- "summary": "Design contributor",
- "link": "https://t.me/ElvanMacko",
- "profile_image": "https://i.imgur.com/w7rnEEw.jpg"
- },
- {
- "name": "Gaming Inc.",
- "summary": "Discord server maintainer",
- "link": "https://discord.gg/qTecXXn",
- "profile_image": "https://i.imgur.com/5wXLrS2.jpg"
- },
- {
- "name": "Marko Ivanović",
- "summary": "Github contributor",
- "link": "https://t.me/markoivanovic",
- "profile_image": "https://avatars2.githubusercontent.com/u/35743654?s=400&u=f8b4a90a352a661625ae71d455a82aaa9bc3316b&v=4"
- },
- {
- "name": "Gaurav Singh",
- "summary": "Telegram and Design contributor",
- "link": "https://t.me/Allstargaurav",
- "profile_image": "https://i.imgur.com/EertxDu.jpg"
- }
-]
\ No newline at end of file
diff --git a/app/src/main/assets/fonts/.DS_Store b/app/src/main/assets/fonts/.DS_Store
deleted file mode 100644
index 5008ddfcf..000000000
Binary files a/app/src/main/assets/fonts/.DS_Store and /dev/null differ
diff --git a/app/src/main/assets/fonts/circular_std_black.otf b/app/src/main/assets/fonts/circular_std_black.otf
deleted file mode 100755
index c62b210c5..000000000
Binary files a/app/src/main/assets/fonts/circular_std_black.otf and /dev/null differ
diff --git a/app/src/main/assets/fonts/circular_std_book.otf b/app/src/main/assets/fonts/circular_std_book.otf
deleted file mode 100755
index 3a1f1ad82..000000000
Binary files a/app/src/main/assets/fonts/circular_std_book.otf and /dev/null differ
diff --git a/app/src/main/assets/images/daksh.png b/app/src/main/assets/images/daksh.png
new file mode 100644
index 000000000..737f9066c
Binary files /dev/null and b/app/src/main/assets/images/daksh.png differ
diff --git a/app/src/main/assets/images/haythem.jpg b/app/src/main/assets/images/haythem.jpg
new file mode 100644
index 000000000..c74527f1d
Binary files /dev/null and b/app/src/main/assets/images/haythem.jpg differ
diff --git a/app/src/main/assets/images/hemanth.jpg b/app/src/main/assets/images/hemanth.jpg
new file mode 100644
index 000000000..b8f5e7805
Binary files /dev/null and b/app/src/main/assets/images/hemanth.jpg differ
diff --git a/app/src/main/assets/images/lenny.jpg b/app/src/main/assets/images/lenny.jpg
new file mode 100644
index 000000000..7f8025f08
Binary files /dev/null and b/app/src/main/assets/images/lenny.jpg differ
diff --git a/app/src/main/assets/images/milind.png b/app/src/main/assets/images/milind.png
new file mode 100644
index 000000000..bbf9b8278
Binary files /dev/null and b/app/src/main/assets/images/milind.png differ
diff --git a/app/src/main/assets/images/pratham.jpg b/app/src/main/assets/images/pratham.jpg
new file mode 100644
index 000000000..7f05ad9d5
Binary files /dev/null and b/app/src/main/assets/images/pratham.jpg differ
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
deleted file mode 100644
index 93fb4aa62..000000000
--- a/app/src/main/assets/index.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Phonograph by Karim Abou Zeid
- RxAndroid by RxAndroid authors
- RxJava by RxJava authors
- Material Dialogs by Aidan Michael Follestad
- Calligraphy by RxJava authors
- Android-Snowfall by JetRadar
- Android Sliding Up Panel by The Umano Team
- AOSP Support Libraries by AOSP contributors
- Butter Knife by Jake Wharton
- Glide by Sam Judd
- Retrofit by Square team
- Material Contextual Action Bar by Aidan Michael Follestad
- OkHttp by Square team
- CircleImageView by Henning Dodenhof
- Transitions Everywhere by Henning Dodenhof
- MaterialProgressBar by Zhang Hai
- Android In-App Billing v3 Library by Henning Dodenhof
- Advanced RecyclerView by Haruki Hasegawa
- Android-ObservableScrollView by Soichiro Kashima
- BottomNavigationViewEx by Ittianyu
- Swipe-Button by EBANX Team
- Font used(CIRCULAR STD BOOK FONT) by NIELSON CAETANO
- Icons by Austin Andrews
- Croller by Harjot Oberai
- Material Design City Wallpaper
-
-
-
diff --git a/app/src/main/assets/license.html b/app/src/main/assets/license.html
new file mode 100644
index 000000000..2fd8ba5ed
--- /dev/null
+++ b/app/src/main/assets/license.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+ Phonograph by
+ Karim Abou Zeid
+AOSP Support Libraries by AOSP contributors
+ Glide by Sam Judd
+ Retrofit by Square team
+
+ OkHttp by Square team
+Koin by Arnaud Giuliani
+ Material Dialogs and Cab
+ by Aidan Michael Follestad
+
+ Material Contextual Action Bar by Aidan Michael Follestad
+ Advanced RecyclerView by Haruki Hasegawa
+Custom Activity on Crash by Eduard Ereza Martínez
+
+NanoHttpd by NanoHttpd Team
+Circular Seekbar by Tankery
+jAudioTagger by Kanedias
+Android Fast Scroll by Zhang Hai
+Image Picker by Dhaval Patel
+Material Intro by Jan Heinrich Reimer
+Slidr by Drew Heavner
+FadingEdgeLayout by bosphere
+KeyboardVisibilityEvent by Yasuhiro SHIMIZU
+android-snowfall by Jetradar Mobile
+Insetter by Chris Banes
+ Icons by Austin Andrews
+ Material Design City Wallpaper
+
+
+
diff --git a/app/src/main/assets/retro-changelog.html b/app/src/main/assets/retro-changelog.html
index 9f5329b34..b6b1c411a 100644
--- a/app/src/main/assets/retro-changelog.html
+++ b/app/src/main/assets/retro-changelog.html
@@ -1 +1,466 @@
-
Version 2.2.100
Click new music mix to play songs
Gradient image option for gird list
Clear button for playing queue
Click toolbar (Library) to open options
Folder list back button
New theme Fit
On library click on toolbar for accessing main menu
On home click on toolbar for accessing search
BottomSheetDialogue is now adaptable to screens, background colour and text size
consistency.
Removed coloured navigation bar option to making app adapt the primary colour
Swipe up gesture for now playing removed, replaced with "tap to open", To achieve
transparent navigation bar for desired themes.
Improved tablet UI and home screen by adding suggestions toggle banner issues.
Improving lyrics page
FAQ's
*If you face any UI related issues you clear app data and cache, if its
not working try to
uninstall and install again.
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
March 30, 2023
+
v6.1.0
+
What's New
+
+ App now targets Android 13, support for Granular media permissions, Photo picker, Per-app language preferences & Predictive back gesture
+
+
Fixed
+
+ Fixed playlist reordering crash
+ Other minor bugs fixes and improvements
+
+
+
+
March 13, 2023
+
v6.0.4
+
What's New
+
+ Minor redesign in Playlist details screen
+ Updated translations
+
+
Fixed
+
+ Fixed file popup menu actions in Folders tab
+ Fixed playlist image loading
+ Fixed blurry album art in Android 13
+ Minor bug fixes and improvements
+
+
+
+
July 10, 2022
+
v6.0.3Beta
+
Fixed
+
+ Migrated icons to Material symbols
+
+
+
+
June 21, 2022
+
v6.0.2Beta
+
Fixed
+
+ Minor bug fixes and improvements
+
+
+
+
June 13, 2022
+
v6.0.1Beta
+
Fixed
+
+ Fixed ChromeCast crash
+ Fixed Slider crashes
+ Fixed storage related crashes on Android 10
+ Fixed CrossFade not working Fade Audio is not working
+
+
+
+
June 7, 2022
+
v6.0.0Beta
+
What's New
+
+ Better Cast
+ Mini player in settings screen
+ Changed Seekbar with Sliders
+ Added NavigationRailView for Landscape
+ Show remaining time in Sleep timer dialog
+
+
Fixed
+
+ Fixed Top/Recent Artists/Albums not updating (Wrong sort order)
+ Fixed all Blacklist related crashes
+ Fix restart button not working in crash activity
+
+
+
+
May 25, 2022
+
v5.8.5
+
What's New
+
+ Display song images along in the artist and album details pages
+ Removed the Internet permissions and all the associated features
+
+
Fixed
+
+ Fixed crashing occurs during changing screen orientation
+
+
+
+
May 13, 2022
+
v5.8.4
+
What's New
+
+ Added a toggle to enable/disable swipe down to dismiss mini player
+
+
Fixed
+
+ Fixed Playback speed and pitch not working when CrossFade is enabled
+ Fixed crash when adding folders to blacklist
+ Fix bugs in MD3 theme
+
+
+
+
May 07, 2022
+
v5.8.3
+
What's New
+
+ Added a new MD3 now playing theme
+ Swipe down to dismiss Mini player
+ Add support for Just Black with Material You
+
+
Fixed
+
+ Fixed sharing of files from SD Card
+ Fix ChromeCast crash and bugs
+ Fix Audio Crossfade
+ Tried to fix incorrect song data in notification
+
+
+
+
April 8, 2022
+
v5.8.0
+
What's New
+
+
Fixed
+
+ Fixed Classic Notification crash
+ Fixed crash when clicking on Playlist in the Search Tab
+ Fixed settings change not reflecting immediately
+ Fixed shuffle
+ Fixed Song duration not visible in Card & Blur card themes
+ Fixed some Album skip styles
+ Minor bug fixes & UI improvements
+
+
+
+
March 13, 2022
+
v5.7.3
+
What's New
+
+ Added adaptive color in Material now playing theme
+ Added an option to share crash report
+ Added options to clear, pause history
+
+
Fixed
+
+ Adapt Wallpaper accent for better readability
+ Optimized search
+ Made sleep timer precise
+
+
+
+
February 13, 2022
+
v5.7.2Beta
+
What's New
+
+ Animated splash screen on Android 12 devices
+
+
Fixed
+
+ Fixed crash when removing song from Playing queue
+ Fixed lyrics editing crash
+ Fixed shuffle button not working
+ Fixed crash on song deletion
+
+
+
+
February 1, 2022
+
v5.7.1Beta
+
What's New
+
+ Added option to disable changing song by swiping anywhere on the now playing screen
+
+
Fixed
+
+ Fixed Playlist save on A11+
+ Fixed Just Black theme
+ Fixed some UI issues
+
+
Improved
+
+ Improvements to search
+
+
+
+
January 25, 2022
+
v5.7.0Beta
+
What's New
+
+ Added Android Auto
+ Added accent color extraction on Android 8.1+ devices
+ Added new Circle widget
+ Added Collapsing appbar to library tabs with an option to switch back to simple appbar
+
+ Added Search tab
+ Added option to use circular play button
+ Added lyrics editing on A11+ devices again
+ Added Long Press to forward, rewind current song
+ Added ability to set Playback speed and pitch
+ Added option to show lyrics over Cover
+ Added option to keep screen on when showing lyrics
+ Added option to switch to Manrope font
+
+
Fixed
+
+ Fixed Gapless Playback
+ Fixed Shuffle FAB going behind Mini Player
+ Fixed crashes on Pre-marshmallow devices
+ Blacklisted songs can't be played after opening from outside the app
+ Fixed various small bugs and some minor improvements
+
+
+
+
January 1, 2021
+
v5.6.1
+
Fixed
+
+ Fixed artist covers not updating and showing album cover images.
+ Fixed FAB's not visible (Shuffle, Save, etc.)
+ Fixed a crash when a Song is deleted in Artist Details
+ Fixed Snowfall effect
+ Fixed empty notification when queue is cleared
+
+
+
+
December 25, 2021
+
v5.6.0Beta
+
What's New
+
+ Added Artwork editing for songs
+ Circle theme has album art now
+ Upgraded tag editor library, we should support reading tags of more formats now e.g.
+ opus.
+
+ Added Snowfall effect
+ Crossfade effect for background when Song is changed for Blur, Blur card themes
+ Album art is hidden when Show lyrics is enabled
+ Added fastscroll in Playlists tab
+ Added Chooser to choose what to restore
+ Hide BottomNavigation when only one tab is selected in Library Categories(This was
+ already there but when changing screens it was getting visible)
+
+
+
Fixed
+
+ Fixed Ringtone crash
+ Fixed blank album cover bug
+ Fixed bottom navigation visible in Playing Queue
+ Fixed lockscreen dragging glitch
+ Fixed incorrect colors when no cover art is available
+ Fixed favorite not updating when song is changed
+ Fixed playlist not getting created & playlist creation crash with same name
+ Fixed a bug in "Plain" Now playing theme where onClick event is consumed by the views
+ behind the bottom sheet
+
+
+
+
+
December 6, 2021
+
v5.4.2Beta
+
Fixed
+
+
+
+
December 1, 2021
+
v5.4.1Beta
+
What's New
+
+ Added file selection from system file picker for restore
+ Added Grid size selection for Playlists
+
+
Improved
+
+ Enable Material You by default on Android 12
+ Reworked Grid Style saving
+ Improved Playlist preview images
+ UI improvements
+
+
Improved
+
+ Fixed File deletion on Android 10
+ Fixed Sleep Timer crash
+ Fixed Bottom Toolbar not clickable in now playing when Shuffle is clicked
+ Fixed Album Artist sort order
+ Fixed a crash when clicking the "Clear All" button in the Blacklist folder selection
+
+ Fixed Continuous crashes on A12 when the song ends
+ Fixed New Music Mix multiple clicks crash
+
+
+
+
November 22, 2021
+
v5.4.0Beta
+
What's New
+
+ Material You
+ Going Edge-to-Edge
+ Added Backup & restore
+
+
Improved
+
+ Improved Animations
+ Improved Crossfade
+
+
+
+
September 06, 2021
+
v5.0.0
+
What's New
+
+ Added ability to Remember last tab
+ Added Whitelisting songs
+ You can now browse SDCard from Folders Tab
+
+
+
+
August 22, 2021
+
v4.4.0Beta
+
What's New
+
+ Added Crossfade
+ Multi-select in Album and Artist Details
+ Albums now show Album Artists instead of artist of first song
+
+
Fixed
+
+ Fixed Playlist Preview Images
+
+
+
+
August 11, 2021
+
v4.2.30Beta
+
What's New
+
+ Revamped Playlist Tab
+ Revamped Genres Tab
+ Added support for embedded Synced Lyrics
+ Added animated icons
+
+
Fixed
+
+ Fixed Language Switching
+ Fixed some reported bugs
+
+
+
+
July 18, 2021
+
v4.2.020Beta
+
What's New
+
+ Added ChromeCast Support
+ Added Lyrics Editor for Normal and Synced Lyrics
+ Added Ripple Animation for Color Theme
+ Added Drag to seek in Tiny Theme
+ Added Playlist Order
+ Added Search Filters
+ Added Audio Fade
+ Synced Lyrics in all Themes
+ Swipe anywhere to change song
+
+
Improved
+
+ Fixed Navigate by Album Artist
+ Changed New Music Mix Actions
+ Improved Animations
+ And some minor bug fixes and improvements
+
+
+
+
October 12, 2020
+
v4.0.10
+
What's New
+
+ Re-built from scratch using MVVM Architecture and JetPack Components
+ New Material Design icon
+ Implemented a custom database for playlists
+ Added new Material Design motions
+ Bug fixes & performance improvements
+ Revamped Home tab UI
+ Android 11 support
+ And more!
+
+
+
+
April 30, 2020
+
v3.5.110Beta
+
What's New
+
+ Changed profile form image to icon
+ New what's new screen
+ Added In-App language changer, where you can select language
+
+
Improved
+
+ Improved loading of Songs, Albums, Artists, Genres, Playlists
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 000000000..aa6e4ae9a
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
index 69c935df1..7e7647db5 100644
Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/.DS_Store b/app/src/main/java/.DS_Store
deleted file mode 100644
index f8a438702..000000000
Binary files a/app/src/main/java/.DS_Store and /dev/null differ
diff --git a/app/src/main/java/code/.DS_Store b/app/src/main/java/code/.DS_Store
deleted file mode 100644
index 366e4258b..000000000
Binary files a/app/src/main/java/code/.DS_Store and /dev/null differ
diff --git a/app/src/main/java/code/name/.DS_Store b/app/src/main/java/code/name/.DS_Store
deleted file mode 100644
index 8e8fae656..000000000
Binary files a/app/src/main/java/code/name/.DS_Store and /dev/null differ
diff --git a/app/src/main/java/code/name/monkey/.DS_Store b/app/src/main/java/code/name/monkey/.DS_Store
deleted file mode 100644
index 6fbb1c5ff..000000000
Binary files a/app/src/main/java/code/name/monkey/.DS_Store and /dev/null differ
diff --git a/app/src/main/java/code/name/monkey/retromusic/.DS_Store b/app/src/main/java/code/name/monkey/retromusic/.DS_Store
deleted file mode 100644
index b4e634739..000000000
Binary files a/app/src/main/java/code/name/monkey/retromusic/.DS_Store and /dev/null differ
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 0c3d590dc..93dfe4d96 100644
--- a/app/src/main/java/code/name/monkey/retromusic/App.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/App.kt
@@ -1,93 +1,74 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.content.Context
-import androidx.multidex.MultiDexApplication
+import android.app.Application
+import androidx.preference.PreferenceManager
+import cat.ereza.customactivityoncrash.config.CaocConfig
import code.name.monkey.appthemehelper.ThemeStore
-import com.anjlab.android.iab.v3.BillingProcessor
-import com.anjlab.android.iab.v3.TransactionDetails
-import com.bumptech.glide.Glide
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.activities.ErrorActivity
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appshortcuts.DynamicShortcutManager
+import code.name.monkey.retromusic.helper.WallpaperAccentManager
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
-class App : MultiDexApplication() {
+class App : Application() {
- lateinit var billingProcessor: BillingProcessor
+ private val wallpaperAccentManager = WallpaperAccentManager(this)
override fun onCreate() {
super.onCreate()
instance = this
- setupErrorHandler()
-
+ startKoin {
+ androidContext(this@App)
+ modules(appModules)
+ }
// default theme
if (!ThemeStore.isConfigured(this, 3)) {
ThemeStore.editTheme(this)
- .accentColorRes(R.color.md_green_A200)
- .coloredNavigationBar(true)
- .commit()
+ .accentColorRes(code.name.monkey.appthemehelper.R.color.md_deep_purple_A200)
+ .coloredNavigationBar(true)
+ .commit()
}
+ wallpaperAccentManager.init()
- // automatically restores purchases
- billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSE_KEY,
- object : BillingProcessor.IBillingHandler {
- override fun onProductPurchased(productId: String, details: TransactionDetails?) {}
+ if (VersionUtils.hasNougatMR())
+ DynamicShortcutManager(this).initDynamicShortcuts()
- override fun onPurchaseHistoryRestored() {
- //Toast.makeText(App.this, R.string.restored_previous_purchase_please_restart, Toast.LENGTH_LONG).show();
- }
+ // setting Error activity
+ CaocConfig.Builder.create().errorActivity(ErrorActivity::class.java)
+ .restartActivity(MainActivity::class.java).apply()
- override fun onBillingError(errorCode: Int, error: Throwable?) {}
-
- override fun onBillingInitialized() {}
- })
- }
-
- private fun setupErrorHandler() {
- Thread.setDefaultUncaughtExceptionHandler { _, throwable -> handleUncaughtException(throwable) }
- }
-
- private fun handleUncaughtException(throwable: Throwable) {
- throwable.printStackTrace()
- deleteAppData()
- //Intent intent = new Intent(this, ErrorHandlerActivity.class);
- //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- //startActivity(intent);
- }
-
- override fun onLowMemory() {
- super.onLowMemory()
- Glide.with(this).onLowMemory()
+ // Set Default values for now playing preferences
+ // This will reduce startup time for now playing settings fragment as Preference listener of AbsSlidingMusicPanelActivity won't be called
+ PreferenceManager.setDefaultValues(this, R.xml.pref_now_playing_screen, false)
}
override fun onTerminate() {
super.onTerminate()
- billingProcessor.release()
+ wallpaperAccentManager.release()
}
companion object {
+ private var instance: App? = null
- const val PRO_VERSION_PRODUCT_ID = "pro_version"
-
- lateinit var instance: App
- private set
-
- val context: Context
- get() = instance.applicationContext
-
- val isProVersion: Boolean
- get() = BuildConfig.DEBUG || instance.billingProcessor.isPurchased(PRO_VERSION_PRODUCT_ID)
-
- fun deleteAppData() {
- try {
- // clearing app data
- val packageName = instance.packageName
- val runtime = Runtime.getRuntime()
- runtime.exec("pm clear $packageName")
-
- System.exit(0)
-
- } catch (e: Exception) {
- e.printStackTrace()
- }
-
+ fun getContext(): App {
+ return instance!!
}
}
}
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 38f3bfb84..31032f62b 100644
--- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt
@@ -1,97 +1,154 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
import android.provider.MediaStore
object Constants {
+ const val TRANSLATE = "https://crowdin.com/project/retromusicplayer"
+ const val GITHUB_PROJECT = "https://github.com/MuntashirAkon/Metro"
+ const val TELEGRAM_CHANGE_LOG = "https://t.me/AppManagerChannel"
+ const val USER_PROFILE = "profile.jpg"
+ const val USER_BANNER = "banner.jpg"
+ const val FAQ_LINK = "https://github.com/MuntashirAkon/Metro/blob/master/FAQ.md"
- @JvmField
- val DISCORD_LINK = "https://discord.gg/qTecXXn"
+ const val IS_MUSIC =
+ MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"
- @JvmField
- val RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"
- @JvmField
- val MUSIC_PACKAGE_NAME = "com.android.music"
- @JvmField
- val ACTION_TOGGLE_PAUSE = "$RETRO_MUSIC_PACKAGE_NAME.togglepause"
- @JvmField
- val ACTION_PLAY = "$RETRO_MUSIC_PACKAGE_NAME.play"
- @JvmField
- val ACTION_PLAY_PLAYLIST = "$RETRO_MUSIC_PACKAGE_NAME.play.playlist"
- @JvmField
- val ACTION_PAUSE = "$RETRO_MUSIC_PACKAGE_NAME.pause"
- @JvmField
- val ACTION_STOP = "$RETRO_MUSIC_PACKAGE_NAME.stop"
- @JvmField
- val ACTION_SKIP = "$RETRO_MUSIC_PACKAGE_NAME.skip"
- @JvmField
- val ACTION_REWIND = "$RETRO_MUSIC_PACKAGE_NAME.rewind"
- @JvmField
- val ACTION_QUIT = "$RETRO_MUSIC_PACKAGE_NAME.quitservice"
- @JvmField
- val INTENT_EXTRA_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"
- @JvmField
- val INTENT_EXTRA_SHUFFLE_MODE = "$RETRO_MUSIC_PACKAGE_NAME.intentextra.shufflemode"
- @JvmField
- val APP_WIDGET_UPDATE = "$RETRO_MUSIC_PACKAGE_NAME.appwidgetupdate"
- @JvmField
- val EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"
+ const val DATA = "_data"
- @JvmField
- val META_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.metachanged"
- @JvmField
- val QUEUE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.queuechanged"
- @JvmField
- val PLAY_STATE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.playstatechanged"
- @JvmField
- val REPEAT_MODE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.repeatmodechanged"
- @JvmField
- val SHUFFLE_MODE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.shufflemodechanged"
- @JvmField
- val MEDIA_STORE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.mediastorechanged"
- @JvmField
- val RATE_ON_GOOGLE_PLAY = "https://play.google.com/store/apps/details?id=code.name.monkey.retromusic"
- @JvmField
- val PAYPAL_ME_URL = "https://www.paypal.me/h4h14"
- @JvmField
- val GOOGLE_PLUS_COMMUNITY = "https://plus.google.com/communities/110811566242871492162"
- @JvmField
- val TRANSLATE = "http://monkeycodeapp.oneskyapp.com/collaboration/project?id=238534"
- @JvmField
- val GITHUB_PROJECT = "https://github.com/h4h13/RetroMusicPlayer"
- @JvmField
- val BASE_API_URL_KUGOU = "http://lyrics.kugou.com/"
- @JvmField
- val TELEGRAM_CHANGE_LOG = "https://t.me/retromusiclog"
- @JvmField
- val USER_PROFILE = "profile.jpg"
- @JvmField
- val USER_BANNER = "banner.jpg"
- @JvmField
- val APP_INSTAGRAM_LINK = "https://www.instagram.com/retromusicapp/"
- @JvmField
- val APP_TELEGRAM_LINK = "https://t.me/retromusicapp/"
- @JvmField
- val APP_TWITTER_LINK = "https://twitter.com/retromusicapp"
- @JvmField
- val FAQ_LINK = "https://github.com/h4h13/RetroMusicPlayer/blob/master/FAQ.md"
- @JvmField
- val CAST_SERVER_PORT = 8080
-
- const val BASE_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"
- @JvmField
- val BASE_PROJECTION = arrayOf(BaseColumns._ID, // 0
- MediaStore.Audio.AudioColumns.TITLE, // 1
- MediaStore.Audio.AudioColumns.TRACK, // 2
- MediaStore.Audio.AudioColumns.YEAR, // 3
- MediaStore.Audio.AudioColumns.DURATION, // 4
- MediaStore.Audio.AudioColumns.DATA, // 5
- MediaStore.Audio.AudioColumns.DATE_MODIFIED, // 6
- MediaStore.Audio.AudioColumns.ALBUM_ID, // 7
- MediaStore.Audio.AudioColumns.ALBUM, // 8
- MediaStore.Audio.AudioColumns.ARTIST_ID, // 9
- MediaStore.Audio.AudioColumns.ARTIST)// 10
+ @Suppress("Deprecation")
+ val baseProjection = arrayOf(
+ BaseColumns._ID, // 0
+ MediaStore.Audio.AudioColumns.TITLE, // 1
+ MediaStore.Audio.AudioColumns.TRACK, // 2
+ MediaStore.Audio.AudioColumns.YEAR, // 3
+ MediaStore.Audio.AudioColumns.DURATION, // 4
+ DATA, // 5
+ MediaStore.Audio.AudioColumns.DATE_MODIFIED, // 6
+ 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
+ )
const val NUMBER_OF_TOP_TRACKS = 99
-
-
}
+
+const val EXTRA_PLAYLIST_TYPE = "type"
+const val EXTRA_GENRE = "extra_genre"
+const val EXTRA_PLAYLIST = "extra_playlist"
+const val EXTRA_PLAYLIST_ID = "extra_playlist_id"
+const val EXTRA_ALBUM_ID = "extra_album_id"
+const val EXTRA_ARTIST_ID = "extra_artist_id"
+const val EXTRA_SONG = "extra_songs"
+const val EXTRA_PLAYLISTS = "extra_playlists"
+const val LIBRARY_CATEGORIES = "library_categories"
+const val EXTRA_SONG_INFO = "extra_song_info"
+const val DESATURATED_COLOR = "desaturated_color"
+const val BLACK_THEME = "black_theme"
+const val KEEP_SCREEN_ON = "keep_screen_on"
+const val TOGGLE_HOME_BANNER = "toggle_home_banner"
+const val NOW_PLAYING_SCREEN_ID = "now_playing_screen_id"
+const val CAROUSEL_EFFECT = "carousel_effect"
+const val COLORED_NOTIFICATION = "colored_notification"
+const val CLASSIC_NOTIFICATION = "classic_notification"
+const val GAP_LESS_PLAYBACK = "gapless_playback"
+const val ALBUM_ART_ON_LOCK_SCREEN = "album_art_on_lock_screen"
+const val BLURRED_ALBUM_ART = "blurred_album_art"
+const val NEW_BLUR_AMOUNT = "new_blur_amount"
+const val TOGGLE_HEADSET = "toggle_headset"
+const val GENERAL_THEME = "general_theme"
+const val ACCENT_COLOR = "accent_color"
+const val SHOULD_COLOR_APP_SHORTCUTS = "should_color_app_shortcuts"
+const val CIRCULAR_ALBUM_ART = "circular_album_art"
+const val USER_NAME = "user_name"
+const val TOGGLE_FULL_SCREEN = "toggle_full_screen"
+const val TOGGLE_VOLUME = "toggle_volume"
+const val ADAPTIVE_COLOR_APP = "adaptive_color_app"
+const val HOME_ARTIST_GRID_STYLE = "home_artist_grid_style"
+const val HOME_ALBUM_GRID_STYLE = "home_album_grid_style"
+const val TOGGLE_ADD_CONTROLS = "toggle_add_controls"
+const val ALBUM_COVER_STYLE = "album_cover_style_id"
+const val ALBUM_COVER_TRANSFORM = "album_cover_transform"
+const val TAB_TEXT_MODE = "tab_text_mode"
+const val LANGUAGE_NAME = "language_name"
+const val LOCALE_AUTO_STORE_ENABLED = "locale_auto_store_enabled"
+const val SLEEP_TIMER_FINISH_SONG = "sleep_timer_finish_song"
+const val ALBUM_GRID_STYLE = "album_grid_style_home"
+const val ARTIST_GRID_STYLE = "artist_grid_style_home"
+const val SAF_SDCARD_URI = "saf_sdcard_uri"
+const val SONG_SORT_ORDER = "song_sort_order"
+const val SONG_GRID_SIZE = "song_grid_size"
+const val GENRE_SORT_ORDER = "genre_sort_order"
+const val BLUETOOTH_PLAYBACK = "bluetooth_playback"
+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"
+const val ALBUM_GRID_SIZE_LAND = "album_grid_size_land"
+const val SONG_GRID_SIZE_LAND = "song_grid_size_land"
+const val ARTIST_GRID_SIZE = "artist_grid_size"
+const val ARTIST_GRID_SIZE_LAND = "artist_grid_size_land"
+const val PLAYLIST_GRID_SIZE = "playlist_grid_size"
+const val PLAYLIST_GRID_SIZE_LAND = "playlist_grid_size_land"
+const val COLORED_APP_SHORTCUTS = "colored_app_shortcuts"
+const val LAST_ADDED_CUTOFF = "last_added_interval"
+const val LAST_SLEEP_TIMER_VALUE = "last_sleep_timer_value"
+const val NEXT_SLEEP_TIMER_ELAPSED_REALTIME = "next_sleep_timer_elapsed_real_time"
+const val IGNORE_MEDIA_STORE_ARTWORK = "ignore_media_store_artwork"
+const val LAST_CHANGELOG_VERSION = "last_changelog_version"
+const val START_DIRECTORY = "start_directory"
+const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval"
+const val LOCK_SCREEN = "lock_screen"
+const val ALBUM_ARTISTS_ONLY = "album_artists_only"
+const val ALBUM_ARTIST = "album_artist"
+const val ALBUM_DETAIL_SONG_SORT_ORDER = "album_detail_song_sort_order"
+const val ARTIST_DETAIL_SONG_SORT_ORDER = "artist_detail_song_sort_order"
+const val LYRICS_OPTIONS = "lyrics_tab_position"
+const val CHOOSE_EQUALIZER = "choose_equalizer"
+const val EQUALIZER = "equalizer"
+const val 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"
+const val EXTRA_ARTIST_NAME = "extra_artist_name"
+const val TOGGLE_SUGGESTIONS = "toggle_suggestions"
+const val AUDIO_FADE_DURATION = "audio_fade_duration"
+const val CROSS_FADE_DURATION = "cross_fade_duration"
+const val SHOW_LYRICS = "show_lyrics"
+const val REMEMBER_LAST_TAB = "remember_last_tab"
+const val LAST_USED_TAB = "last_used_tab"
+const val WHITELIST_MUSIC = "whitelist_music"
+const val MATERIAL_YOU = "material_you"
+const val SNOWFALL = "snowfall"
+const val LYRICS_TYPE = "lyrics_type"
+const val PLAYBACK_SPEED = "playback_speed"
+const val PLAYBACK_PITCH = "playback_pitch"
+const val CUSTOM_FONT = "custom_font"
+const val APPBAR_MODE = "appbar_mode"
+const val WALLPAPER_ACCENT = "wallpaper_accent"
+const val SCREEN_ON_LYRICS = "screen_on_lyrics"
+const val CIRCLE_PLAY_BUTTON = "circle_play_button"
+const val SWIPE_ANYWHERE_NOW_PLAYING = "swipe_anywhere_now_playing"
+const val PAUSE_HISTORY = "pause_history"
+const val MANAGE_AUDIO_FOCUS = "manage_audio_focus"
+const val SWIPE_DOWN_DISMISS = "swipe_to_dismiss"
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt
new file mode 100644
index 000000000..4f1273a03
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/HomeSection.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+
+@IntDef(
+ RECENT_ALBUMS,
+ TOP_ALBUMS,
+ RECENT_ARTISTS,
+ TOP_ARTISTS,
+ SUGGESTIONS,
+ FAVOURITES,
+ GENRES,
+ PLAYLISTS
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class HomeSection
+
+const val RECENT_ALBUMS = 3
+const val TOP_ALBUMS = 1
+const val RECENT_ARTISTS = 2
+const val TOP_ARTISTS = 0
+const val SUGGESTIONS = 5
+const val FAVOURITES = 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
diff --git a/app/src/main/java/code/name/monkey/retromusic/Injection.kt b/app/src/main/java/code/name/monkey/retromusic/Injection.kt
deleted file mode 100644
index 349d029f0..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/Injection.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package code.name.monkey.retromusic
-
-import code.name.monkey.retromusic.providers.RepositoryImpl
-import code.name.monkey.retromusic.providers.interfaces.Repository
-import code.name.monkey.retromusic.rest.KogouClient
-import code.name.monkey.retromusic.rest.service.KuGouApiService
-import code.name.monkey.retromusic.util.schedulers.BaseSchedulerProvider
-import code.name.monkey.retromusic.util.schedulers.SchedulerProvider
-
-object Injection {
-
- fun provideRepository(): Repository {
- return RepositoryImpl.getInstance()
- }
-
- fun provideSchedulerProvider(): BaseSchedulerProvider {
- return SchedulerProvider.getInstance()
- }
-
- fun provideKuGouApiService(): KuGouApiService {
- return KogouClient().apiService
- }
-}
diff --git a/app/src/main/java/code/name/monkey/retromusic/MainModule.kt b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt
new file mode 100644
index 000000000..d88f8a760
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/MainModule.kt
@@ -0,0 +1,156 @@
+package code.name.monkey.retromusic
+
+import androidx.room.Room
+import code.name.monkey.retromusic.auto.AutoMusicProvider
+import code.name.monkey.retromusic.db.MIGRATION_23_24
+import code.name.monkey.retromusic.db.RetroDatabase
+import code.name.monkey.retromusic.fragments.LibraryViewModel
+import code.name.monkey.retromusic.fragments.albums.AlbumDetailsViewModel
+import code.name.monkey.retromusic.fragments.artists.ArtistDetailsViewModel
+import code.name.monkey.retromusic.fragments.genres.GenreDetailsViewModel
+import code.name.monkey.retromusic.fragments.playlists.PlaylistDetailsViewModel
+import code.name.monkey.retromusic.model.Genre
+import code.name.monkey.retromusic.repository.*
+import org.koin.android.ext.koin.androidContext
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+private val roomModule = module {
+
+ single {
+ Room.databaseBuilder(androidContext(), RetroDatabase::class.java, "playlist.db")
+ .addMigrations(MIGRATION_23_24)
+ .build()
+ }
+
+ factory {
+ get().playlistDao()
+ }
+
+ factory {
+ get().playCountDao()
+ }
+
+ factory {
+ get().historyDao()
+ }
+
+ single {
+ RealRoomRepository(get(), get(), get())
+ } bind RoomRepository::class
+}
+private val autoModule = module {
+ single {
+ AutoMusicProvider(
+ androidContext(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get()
+ )
+ }
+}
+private val mainModule = module {
+ single {
+ androidContext().contentResolver
+ }
+}
+private val dataModule = module {
+ single {
+ RealRepository(
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ )
+ } bind Repository::class
+
+ single {
+ RealSongRepository(get())
+ } bind SongRepository::class
+
+ single {
+ RealGenreRepository(get(), get())
+ } bind GenreRepository::class
+
+ single {
+ RealAlbumRepository(get())
+ } bind AlbumRepository::class
+
+ single {
+ RealArtistRepository(get(), get())
+ } bind ArtistRepository::class
+
+ single {
+ RealPlaylistRepository(get())
+ } bind PlaylistRepository::class
+
+ single {
+ RealTopPlayedRepository(get(), get(), get(), get())
+ } bind TopPlayedRepository::class
+
+ single {
+ RealLastAddedRepository(
+ get(),
+ get(),
+ get()
+ )
+ } bind LastAddedRepository::class
+
+ single {
+ RealSearchRepository(
+ get(),
+ get(),
+ get(),
+ get(),
+ get()
+ )
+ }
+}
+
+private val viewModules = module {
+
+ viewModel {
+ LibraryViewModel(get())
+ }
+
+ viewModel { (albumId: Long) ->
+ AlbumDetailsViewModel(
+ get(),
+ albumId
+ )
+ }
+
+ viewModel { (artistId: Long?, artistName: String?) ->
+ ArtistDetailsViewModel(
+ get(),
+ artistId,
+ artistName
+ )
+ }
+
+ viewModel { (playlistId: Long) ->
+ PlaylistDetailsViewModel(
+ get(),
+ playlistId
+ )
+ }
+
+ viewModel { (genre: Genre) ->
+ GenreDetailsViewModel(
+ get(),
+ genre
+ )
+ }
+}
+
+val appModules = listOf(mainModule, dataModule, autoModule, viewModules, roomModule)
\ No newline at end of file
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
new file mode 100644
index 000000000..af3890d22
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.os.Bundle
+import androidx.lifecycle.lifecycleScope
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
+import code.name.monkey.retromusic.databinding.ActivityDriveModeBinding
+import code.name.monkey.retromusic.db.toSongEntity
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.drawAboveSystemBars
+import code.name.monkey.retromusic.glide.BlurTransformation
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
+import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper.Callback
+import code.name.monkey.retromusic.helper.PlayPauseButtonOnClickHandler
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.repository.RealRepository
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.util.MusicUtil
+import com.bumptech.glide.Glide
+import com.google.android.material.slider.Slider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
+
+
+/**
+ * Created by hemanths on 2020-02-02.
+ */
+
+class DriveModeActivity : AbsMusicServiceActivity(), Callback {
+
+ private lateinit var binding: ActivityDriveModeBinding
+ private var lastPlaybackControlsColor: Int = Color.GRAY
+ private var lastDisabledPlaybackControlsColor: Int = Color.GRAY
+ private lateinit var progressViewUpdateHelper: MusicProgressViewUpdateHelper
+ private val repository: RealRepository by inject()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityDriveModeBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setUpMusicControllers()
+
+ progressViewUpdateHelper = MusicProgressViewUpdateHelper(this)
+ lastPlaybackControlsColor = accentColor()
+ binding.close.setOnClickListener {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ binding.repeatButton.drawAboveSystemBars()
+ }
+
+ private fun setUpMusicControllers() {
+ setUpPlayPauseFab()
+ setUpPrevNext()
+ setUpRepeatButton()
+ setUpShuffleButton()
+ setUpProgressSlider()
+ setupFavouriteToggle()
+ }
+
+ private fun setupFavouriteToggle() {
+ binding.songFavourite.setOnClickListener {
+ toggleFavorite(MusicPlayerRemote.currentSong)
+ }
+ }
+
+ private fun toggleFavorite(song: Song) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val playlist = repository.favoritePlaylist()
+ val songEntity = song.toSongEntity(playlist.playListId)
+ val isFavorite = repository.isSongFavorite(song.id)
+ if (isFavorite) {
+ repository.removeSongFromPlaylist(songEntity)
+ } else {
+ repository.insertSongs(listOf(song.toSongEntity(playlist.playListId)))
+ }
+ sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
+ }
+ }
+
+ private fun updateFavorite() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val isFavorite: Boolean =
+ repository.isSongFavorite(MusicPlayerRemote.currentSong.id)
+ withContext(Dispatchers.Main) {
+ binding.songFavourite.setImageResource(if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border)
+ }
+ }
+ }
+
+ private fun setUpProgressSlider() {
+ binding.progressSlider.addOnChangeListener { _: Slider, progress: Float, fromUser: Boolean ->
+ if (fromUser) {
+ MusicPlayerRemote.seekTo(progress.toInt())
+ onUpdateProgressViews(
+ MusicPlayerRemote.songProgressMillis,
+ MusicPlayerRemote.songDurationMillis
+ )
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ progressViewUpdateHelper.stop()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ progressViewUpdateHelper.start()
+ }
+
+ private fun setUpPrevNext() {
+ binding.nextButton.setOnClickListener { MusicPlayerRemote.playNextSong() }
+ binding.previousButton.setOnClickListener { MusicPlayerRemote.back() }
+ }
+
+ private fun setUpShuffleButton() {
+ binding.shuffleButton.setOnClickListener { MusicPlayerRemote.toggleShuffleMode() }
+ }
+
+ private fun setUpRepeatButton() {
+ binding.repeatButton.setOnClickListener { MusicPlayerRemote.cycleRepeatMode() }
+ }
+
+ private fun setUpPlayPauseFab() {
+ binding.playPauseButton.setOnClickListener(PlayPauseButtonOnClickHandler())
+ }
+
+ override fun onRepeatModeChanged() {
+ super.onRepeatModeChanged()
+ updateRepeatState()
+ }
+
+ override fun onShuffleModeChanged() {
+ super.onShuffleModeChanged()
+ updateShuffleState()
+ }
+
+ override fun onPlayStateChanged() {
+ super.onPlayStateChanged()
+ updatePlayPauseDrawableState()
+ }
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ updatePlayPauseDrawableState()
+ updateSong()
+ updateRepeatState()
+ updateShuffleState()
+ updateFavorite()
+ }
+
+ private fun updatePlayPauseDrawableState() {
+ if (MusicPlayerRemote.isPlaying) {
+ binding.playPauseButton.setImageResource(R.drawable.ic_pause)
+ } else {
+ binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow)
+ }
+ }
+
+ fun updateShuffleState() {
+ when (MusicPlayerRemote.shuffleMode) {
+ MusicService.SHUFFLE_MODE_SHUFFLE -> binding.shuffleButton.setColorFilter(
+ lastPlaybackControlsColor,
+ PorterDuff.Mode.SRC_IN
+ )
+
+ else -> binding.shuffleButton.setColorFilter(
+ lastDisabledPlaybackControlsColor,
+ PorterDuff.Mode.SRC_IN
+ )
+ }
+ }
+
+ private fun updateRepeatState() {
+ when (MusicPlayerRemote.repeatMode) {
+ MusicService.REPEAT_MODE_NONE -> {
+ binding.repeatButton.setImageResource(R.drawable.ic_repeat)
+ binding.repeatButton.setColorFilter(
+ lastDisabledPlaybackControlsColor,
+ PorterDuff.Mode.SRC_IN
+ )
+ }
+
+ MusicService.REPEAT_MODE_ALL -> {
+ binding.repeatButton.setImageResource(R.drawable.ic_repeat)
+ binding.repeatButton.setColorFilter(
+ lastPlaybackControlsColor,
+ PorterDuff.Mode.SRC_IN
+ )
+ }
+
+ MusicService.REPEAT_MODE_THIS -> {
+ binding.repeatButton.setImageResource(R.drawable.ic_repeat_one)
+ binding.repeatButton.setColorFilter(
+ lastPlaybackControlsColor,
+ PorterDuff.Mode.SRC_IN
+ )
+ }
+ }
+ }
+
+ override fun onPlayingMetaChanged() {
+ super.onPlayingMetaChanged()
+ updateSong()
+ updateFavorite()
+ }
+
+ override fun onFavoriteStateChanged() {
+ super.onFavoriteStateChanged()
+ updateFavorite()
+ }
+
+ private fun updateSong() {
+ val song = MusicPlayerRemote.currentSong
+
+ binding.songTitle.text = song.title
+ binding.songText.text = song.artistName
+
+ Glide.with(this)
+ .load(RetroGlideExtension.getSongModel(song))
+ .songCoverOptions(song)
+ .transform(BlurTransformation.Builder(this).build())
+ .into(binding.image)
+ }
+
+ override fun onUpdateProgressViews(progress: Int, total: Int) {
+ binding.progressSlider.run {
+ valueTo = total.toFloat()
+ value = progress.toFloat().coerceIn(valueFrom, valueTo)
+ }
+
+ binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong())
+ binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong())
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/ErrorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/ErrorActivity.kt
new file mode 100644
index 000000000..6823b80f8
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/ErrorActivity.kt
@@ -0,0 +1,80 @@
+package code.name.monkey.retromusic.activities
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.ImageView
+import androidx.appcompat.app.AppCompatActivity
+import cat.ereza.customactivityoncrash.CustomActivityOnCrash
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.util.FileUtils.createFile
+import code.name.monkey.retromusic.util.Share.shareFile
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+
+class ErrorActivity : AppCompatActivity() {
+ private val dayFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+ private val reportPrefix = "bug_report-"
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(cat.ereza.customactivityoncrash.R.layout.customactivityoncrash_default_error_activity)
+
+ val restartButton =
+ findViewById(cat.ereza.customactivityoncrash.R.id.customactivityoncrash_error_activity_restart_button)
+
+ val config = CustomActivityOnCrash.getConfigFromIntent(intent)
+ if (config == null) {
+ finish()
+ return
+ }
+ restartButton.setText(cat.ereza.customactivityoncrash.R.string.customactivityoncrash_error_activity_restart_app)
+ restartButton.setOnClickListener {
+ CustomActivityOnCrash.restartApplication(
+ this@ErrorActivity,
+ config
+ )
+ }
+ val moreInfoButton =
+ findViewById(cat.ereza.customactivityoncrash.R.id.customactivityoncrash_error_activity_more_info_button)
+
+ moreInfoButton.setOnClickListener { //We retrieve all the error data and show it
+ MaterialAlertDialogBuilder(this@ErrorActivity)
+ .setTitle(cat.ereza.customactivityoncrash.R.string.customactivityoncrash_error_activity_error_details_title)
+ .setMessage(
+ CustomActivityOnCrash.getAllErrorDetailsFromIntent(
+ this@ErrorActivity,
+ intent
+ )
+ )
+ .setPositiveButton(
+ cat.ereza.customactivityoncrash.R.string.customactivityoncrash_error_activity_error_details_close,
+ null
+ )
+ .setNeutralButton(
+ R.string.customactivityoncrash_error_activity_error_details_share
+ ) { _, _ ->
+
+ val bugReport = createFile(
+ context = this,
+ "Bug Report",
+ "$reportPrefix${dayFormat.format(Date())}",
+ CustomActivityOnCrash.getAllErrorDetailsFromIntent(
+ this@ErrorActivity,
+ intent
+ ), ".txt"
+ )
+ shareFile(this, bugReport, "text/*")
+ }
+ .show()
+ }
+ val errorActivityDrawableId = config.errorDrawable
+ val errorImageView =
+ findViewById(cat.ereza.customactivityoncrash.R.id.customactivityoncrash_error_activity_image)
+ if (errorActivityDrawableId != null) {
+ errorImageView.setImageResource(
+ errorActivityDrawableId
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.kt
new file mode 100644
index 000000000..e41352dae
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/LicenseActivity.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+package code.name.monkey.retromusic.activities
+
+import android.graphics.Color
+import android.os.Bundle
+import android.view.MenuItem
+import code.name.monkey.appthemehelper.util.ATHUtil.isWindowBackgroundDark
+import code.name.monkey.appthemehelper.util.ColorUtil.lightenColor
+import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
+import code.name.monkey.retromusic.activities.base.AbsThemeActivity
+import code.name.monkey.retromusic.databinding.ActivityLicenseBinding
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.drawAboveSystemBars
+import code.name.monkey.retromusic.extensions.surfaceColor
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+
+/** Created by hemanths on 2019-09-27. */
+class LicenseActivity : AbsThemeActivity() {
+ private lateinit var binding: ActivityLicenseBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityLicenseBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setSupportActionBar(binding.toolbar)
+ ToolbarContentTintHelper.colorBackButton(binding.toolbar)
+ try {
+ val buf = StringBuilder()
+ val json = assets.open("license.html")
+ BufferedReader(InputStreamReader(json, StandardCharsets.UTF_8)).use { br ->
+ var str: String?
+ while (br.readLine().also { str = it } != null) {
+ buf.append(str)
+ }
+ }
+
+ // Inject color values for WebView body background and links
+ val isDark = isWindowBackgroundDark(this)
+ val backgroundColor = colorToCSS(
+ surfaceColor(Color.parseColor(if (isDark) "#424242" else "#ffffff"))
+ )
+ val contentColor = colorToCSS(Color.parseColor(if (isDark) "#ffffff" else "#000000"))
+ val changeLog = buf.toString()
+ .replace(
+ "{style-placeholder}", String.format(
+ "body { background-color: %s; color: %s; }", backgroundColor, contentColor
+ )
+ )
+ .replace("{link-color}", colorToCSS(accentColor()))
+ .replace(
+ "{link-color-active}",
+ colorToCSS(
+ lightenColor(accentColor())
+ )
+ )
+ binding.license.loadData(changeLog, "text/html", "UTF-8")
+ } catch (e: Throwable) {
+ binding.license.loadData(
+ "Unable to load " + e.localizedMessage + "
", "text/html", "UTF-8"
+ )
+ }
+ binding.license.drawAboveSystemBars()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ onBackPressedDispatcher.onBackPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun colorToCSS(color: Int): String {
+ 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
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..3ac219871
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/LockScreenActivity.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.core.content.getSystemService
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
+import code.name.monkey.retromusic.databinding.ActivityLockScreenBinding
+import code.name.monkey.retromusic.extensions.hideStatusBar
+import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
+import code.name.monkey.retromusic.extensions.whichFragment
+import code.name.monkey.retromusic.fragments.player.lockscreen.LockScreenControlsFragment
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+import com.r0adkll.slidr.Slidr
+import com.r0adkll.slidr.model.SlidrConfig
+import com.r0adkll.slidr.model.SlidrListener
+import com.r0adkll.slidr.model.SlidrPosition
+
+class LockScreenActivity : AbsMusicServiceActivity() {
+ private lateinit var binding: ActivityLockScreenBinding
+ private var fragment: LockScreenControlsFragment? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lockScreenInit()
+ binding = ActivityLockScreenBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ hideStatusBar()
+ setTaskDescriptionColorAuto()
+
+ val config = SlidrConfig.Builder().listener(object : SlidrListener {
+ override fun onSlideStateChanged(state: Int) {
+ }
+
+ override fun onSlideChange(percent: Float) {
+ }
+
+ override fun onSlideOpened() {
+ }
+
+ override fun onSlideClosed(): Boolean {
+ if (VersionUtils.hasOreo()) {
+ val keyguardManager =
+ getSystemService()
+ keyguardManager?.requestDismissKeyguard(this@LockScreenActivity, null)
+ }
+ finish()
+ return true
+ }
+ }).position(SlidrPosition.BOTTOM).build()
+
+ Slidr.attach(this, config)
+
+ fragment = whichFragment(R.id.playback_controls_fragment)
+
+ binding.slide.apply {
+ translationY = 100f
+ alpha = 0f
+ animate().translationY(0f).alpha(1f).setDuration(1500).start()
+ }
+ }
+
+ @Suppress("Deprecation")
+ private fun lockScreenInit() {
+ if (VersionUtils.hasOreoMR1()) {
+ setShowWhenLocked(true)
+ //setTurnScreenOn(true)
+ } else {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ // or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
+ }
+ }
+
+ override fun onPlayingMetaChanged() {
+ super.onPlayingMetaChanged()
+ updateSongs()
+ }
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ updateSongs()
+ }
+
+ private fun updateSongs() {
+ val song = MusicPlayerRemote.currentSong
+ Glide.with(this)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ .dontAnimate()
+ .into(object : RetroMusicColoredTarget(binding.image) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ fragment?.setColor(colors)
+ }
+ })
+ }
+}
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
new file mode 100644
index 000000000..e9cb29abe
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.net.Uri
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.contains
+import androidx.navigation.ui.setupWithNavController
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsSlidingMusicPanelActivity
+import code.name.monkey.retromusic.extensions.*
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.helper.SearchQueryHelper.getSongs
+import code.name.monkey.retromusic.interfaces.IScrollHelper
+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.util.PreferenceUtil
+import code.name.monkey.retromusic.util.logE
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.get
+
+class MainActivity : AbsSlidingMusicPanelActivity() {
+ companion object {
+ const val TAG = "MainActivity"
+ const val EXPAND_PANEL = "expand_panel"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setTaskDescriptionColorAuto()
+ hideStatusBar()
+ updateTabs()
+
+ setupNavigationController()
+
+ WhatsNewFragment.showChangeLog(this)
+ }
+
+ private fun setupNavigationController() {
+ val navController = findNavController(R.id.fragment_container)
+ val navInflater = navController.navInflater
+ val navGraph = navInflater.inflate(R.navigation.main_graph)
+
+ val categoryInfo: CategoryInfo = PreferenceUtil.libraryCategory.first { it.visible }
+ if (categoryInfo.visible) {
+ if (!navGraph.contains(PreferenceUtil.lastTab)) PreferenceUtil.lastTab =
+ categoryInfo.category.id
+ navGraph.setStartDestination(
+ if (PreferenceUtil.rememberLastTab) {
+ PreferenceUtil.lastTab.let {
+ if (it == 0) {
+ categoryInfo.category.id
+ } else {
+ it
+ }
+ }
+ } else categoryInfo.category.id
+ )
+ }
+ navController.graph = navGraph
+ navigationView.setupWithNavController(navController)
+ // Scroll Fragment to top
+ navigationView.setOnItemReselectedListener {
+ currentFragment(R.id.fragment_container).apply {
+ if (this is IScrollHelper) {
+ scrollToTop()
+ }
+ }
+ }
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ if (destination.id == navGraph.startDestinationId) {
+ currentFragment(R.id.fragment_container)?.enterTransition = null
+ }
+ when (destination.id) {
+ R.id.action_home, R.id.action_song, R.id.action_album, R.id.action_artist, R.id.action_folder, R.id.action_playlist, R.id.action_genre, R.id.action_search -> {
+ // Save the last tab
+ if (PreferenceUtil.rememberLastTab) {
+ saveTab(destination.id)
+ }
+ // Show Bottom Navigation Bar
+ setBottomNavVisibility(visible = true, animate = true)
+ }
+ R.id.playing_queue_fragment -> {
+ setBottomNavVisibility(visible = false, hideBottomSheet = true)
+ }
+ else -> setBottomNavVisibility(
+ visible = false,
+ animate = true
+ ) // Hide Bottom Navigation Bar
+ }
+ }
+ }
+
+ private fun saveTab(id: Int) {
+ if (PreferenceUtil.libraryCategory.firstOrNull { it.category.id == id }?.visible == true) {
+ PreferenceUtil.lastTab = id
+ }
+ }
+
+ override fun onSupportNavigateUp(): Boolean =
+ findNavController(R.id.fragment_container).navigateUp()
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ val expand = intent?.extra(EXPAND_PANEL)?.value ?: false
+ if (expand && PreferenceUtil.isExpandPanel) {
+ fromNotification = true
+ slidingPanel.bringToFront()
+ expandPanel()
+ intent?.removeExtra(EXPAND_PANEL)
+ }
+ }
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ intent ?: return
+ handlePlaybackIntent(intent)
+ }
+
+ private fun handlePlaybackIntent(intent: Intent) {
+ lifecycleScope.launch(IO) {
+ val uri: Uri? = intent.data
+ val mimeType: String? = intent.type
+ var handled = false
+ if (intent.action != null &&
+ intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
+ ) {
+ val songs: List = getSongs(intent.extras!!)
+ if (MusicPlayerRemote.shuffleMode == MusicService.SHUFFLE_MODE_SHUFFLE) {
+ MusicPlayerRemote.openAndShuffleQueue(songs, true)
+ } else {
+ MusicPlayerRemote.openQueue(songs, 0, true)
+ }
+ handled = true
+ }
+ if (uri != null && uri.toString().isNotEmpty()) {
+ MusicPlayerRemote.playFromUri(this@MainActivity, uri)
+ handled = true
+ } else if (MediaStore.Audio.Playlists.CONTENT_TYPE == mimeType) {
+ val id = parseLongFromIntent(intent, "playlistId", "playlist")
+ if (id >= 0L) {
+ val position: Int = intent.getIntExtra("position", 0)
+ val songs: List = PlaylistSongsLoader.getPlaylistSongList(get(), id)
+ MusicPlayerRemote.openQueue(songs, position, true)
+ handled = true
+ }
+ } else if (MediaStore.Audio.Albums.CONTENT_TYPE == mimeType) {
+ val id = parseLongFromIntent(intent, "albumId", "album")
+ if (id >= 0L) {
+ val position: Int = intent.getIntExtra("position", 0)
+ val songs = libraryViewModel.albumById(id).songs
+ MusicPlayerRemote.openQueue(
+ songs,
+ position,
+ true
+ )
+ handled = true
+ }
+ } else if (MediaStore.Audio.Artists.CONTENT_TYPE == mimeType) {
+ val id = parseLongFromIntent(intent, "artistId", "artist")
+ if (id >= 0L) {
+ val position: Int = intent.getIntExtra("position", 0)
+ val songs: List = libraryViewModel.artistById(id).songs
+ MusicPlayerRemote.openQueue(
+ songs,
+ position,
+ true
+ )
+ handled = true
+ }
+ }
+ if (handled) {
+ setIntent(Intent())
+ }
+ }
+ }
+
+ private fun parseLongFromIntent(
+ intent: Intent,
+ longKey: String,
+ stringKey: String,
+ ): Long {
+ var id = intent.getLongExtra(longKey, -1)
+ if (id < 0) {
+ val idString = intent.getStringExtra(stringKey)
+ if (idString != null) {
+ try {
+ id = idString.toLong()
+ } catch (e: NumberFormatException) {
+ logE(e)
+ }
+ }
+ }
+ return id
+ }
+}
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
new file mode 100644
index 000000000..5cb64cf93
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/PermissionActivity.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.Manifest.permission.BLUETOOTH_CONNECT
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.ColorStateList
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.net.toUri
+import androidx.core.text.parseAsHtml
+import androidx.core.view.isVisible
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
+import code.name.monkey.retromusic.databinding.ActivityPermissionBinding
+import code.name.monkey.retromusic.extensions.*
+
+class PermissionActivity : AbsMusicServiceActivity() {
+ private lateinit var binding: ActivityPermissionBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityPermissionBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setStatusBarColorAuto()
+ setTaskDescriptionColorAuto()
+ setupTitle()
+
+ binding.storagePermission.setButtonClick {
+ requestPermissions()
+ }
+ if (VersionUtils.hasMarshmallow()) {
+ binding.audioPermission.show()
+ binding.audioPermission.setButtonClick {
+ if (!hasAudioPermission()) {
+ val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
+ intent.data = ("package:" + applicationContext.packageName).toUri()
+ startActivity(intent)
+ }
+ }
+ }
+
+ if (VersionUtils.hasS()) {
+ binding.bluetoothPermission.show()
+ binding.bluetoothPermission.setButtonClick {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(BLUETOOTH_CONNECT),
+ BLUETOOTH_PERMISSION_REQUEST
+ )
+ }
+ } else {
+ binding.audioPermission.setNumber("2")
+ }
+
+ binding.finish.accentBackgroundColor()
+ binding.finish.setOnClickListener {
+ if (hasPermissions()) {
+ startActivity(
+ Intent(this, MainActivity::class.java).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK
+ )
+ )
+ finish()
+ }
+ }
+ onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ finishAffinity()
+ remove()
+ }
+ })
+ }
+
+ private fun setupTitle() {
+ val appName =
+ getString(
+ R.string.message_welcome,
+ "Metro "
+ )
+ .parseAsHtml()
+ binding.appNameText.text = appName
+ }
+
+ override fun onResume() {
+ super.onResume()
+ binding.finish.isEnabled = hasStoragePermission()
+ if (hasStoragePermission()) {
+ binding.storagePermission.checkImage.isVisible = true
+ binding.storagePermission.checkImage.imageTintList =
+ ColorStateList.valueOf(accentColor())
+ }
+ if (VersionUtils.hasMarshmallow()) {
+ if (hasAudioPermission()) {
+ binding.audioPermission.checkImage.isVisible = true
+ binding.audioPermission.checkImage.imageTintList =
+ ColorStateList.valueOf(accentColor())
+ }
+ }
+ if (VersionUtils.hasS()) {
+ if (hasBluetoothPermission()) {
+ binding.bluetoothPermission.checkImage.isVisible = true
+ binding.bluetoothPermission.checkImage.imageTintList =
+ ColorStateList.valueOf(accentColor())
+ }
+ }
+ }
+
+ private fun hasStoragePermission(): Boolean {
+ return hasPermissions()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private fun hasBluetoothPermission(): Boolean {
+ return ActivityCompat.checkSelfPermission(
+ this,
+ BLUETOOTH_CONNECT
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun hasAudioPermission(): Boolean {
+ return Settings.System.canWrite(this)
+ }
+}
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
new file mode 100644
index 000000000..546c9d794
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/ShareInstagramStory.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.provider.MediaStore.Images.Media
+import android.view.MenuItem
+import androidx.core.net.toUri
+import androidx.core.os.BundleCompat
+import androidx.core.view.drawToBitmap
+import code.name.monkey.appthemehelper.util.ColorUtil
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.retromusic.activities.base.AbsThemeActivity
+import code.name.monkey.retromusic.databinding.ActivityShareInstagramBinding
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.setStatusBarColor
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.Share
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+
+/**
+ * Created by hemanths on 2020-02-02.
+ */
+
+class ShareInstagramStory : AbsThemeActivity() {
+
+ private lateinit var binding: ActivityShareInstagramBinding
+
+ companion object {
+ const val EXTRA_SONG = "extra_song"
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ onBackPressedDispatcher.onBackPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityShareInstagramBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setStatusBarColor(Color.TRANSPARENT)
+
+ binding.toolbar.setBackgroundColor(Color.TRANSPARENT)
+ setSupportActionBar(binding.toolbar)
+
+ val song = intent.extras?.let { BundleCompat.getParcelable(it, EXTRA_SONG, Song::class.java) }
+ song?.let { songFinal ->
+ Glide.with(this)
+ .asBitmapPalette()
+ .songCoverOptions(songFinal)
+ .load(RetroGlideExtension.getSongModel(songFinal))
+ .into(object : RetroMusicColoredTarget(binding.image) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(colors.backgroundColor)
+ }
+ })
+
+ binding.shareTitle.text = songFinal.title
+ binding.shareText.text = songFinal.artistName
+ binding.shareButton.setOnClickListener {
+ val path: String = Media.insertImage(
+ contentResolver,
+ binding.mainContent.drawToBitmap(Bitmap.Config.ARGB_8888),
+ "Design", null
+ )
+ Share.shareStoryToSocial(
+ this@ShareInstagramStory,
+ path.toUri()
+ )
+ }
+ }
+ binding.shareButton.setTextColor(
+ MaterialValueHelper.getPrimaryTextColor(
+ this,
+ ColorUtil.isColorLight(accentColor())
+ )
+ )
+ binding.shareButton.backgroundTintList =
+ ColorStateList.valueOf(accentColor())
+ }
+
+ private fun setColors(color: Int) {
+ binding.mainContent.background =
+ GradientDrawable(
+ GradientDrawable.Orientation.TOP_BOTTOM,
+ intArrayOf(color, Color.BLACK)
+ )
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewFragment.kt b/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewFragment.kt
new file mode 100644
index 000000000..48b39ed35
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/WhatsNewFragment.kt
@@ -0,0 +1,153 @@
+package code.name.monkey.retromusic.activities
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.core.content.pm.PackageInfoCompat
+import androidx.core.widget.NestedScrollView
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.appthemehelper.util.ATHUtil.isWindowBackgroundDark
+import code.name.monkey.appthemehelper.util.ColorUtil.isColorLight
+import code.name.monkey.appthemehelper.util.ColorUtil.lightenColor
+import code.name.monkey.appthemehelper.util.MaterialValueHelper.getPrimaryTextColor
+import code.name.monkey.retromusic.BuildConfig
+import code.name.monkey.retromusic.Constants
+import code.name.monkey.retromusic.databinding.FragmentWhatsNewBinding
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.openUrl
+import code.name.monkey.retromusic.util.PreferenceUtil.lastVersion
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import java.nio.charset.StandardCharsets
+import java.util.*
+
+class WhatsNewFragment : BottomSheetDialogFragment() {
+ private var _binding: FragmentWhatsNewBinding? = null
+ val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentWhatsNewBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ try {
+ val buf = StringBuilder()
+ val stream = requireContext().assets.open("retro-changelog.html")
+ stream.reader(StandardCharsets.UTF_8).buffered().use { br ->
+ var str: String?
+ while (br.readLine().also { str = it } != null) {
+ buf.append(str)
+ }
+ }
+
+ // Inject color values for WebView body background and links
+ val isDark = isWindowBackgroundDark(requireContext())
+ val accentColor = accentColor()
+ binding.webView.setBackgroundColor(0)
+ val contentColor = colorToCSS(Color.parseColor(if (isDark) "#ffffff" else "#000000"))
+ val textColor = colorToCSS(Color.parseColor(if (isDark) "#60FFFFFF" else "#80000000"))
+ val accentColorString = colorToCSS(accentColor())
+ val cardBackgroundColor =
+ colorToCSS(Color.parseColor(if (isDark) "#353535" else "#ffffff"))
+ val accentTextColor = colorToCSS(
+ getPrimaryTextColor(
+ requireContext(), isColorLight(accentColor)
+ )
+ )
+ val changeLog = buf.toString()
+ .replace(
+ "{style-placeholder}",
+ "body { color: $contentColor; } li {color: $textColor;} h3 {color: $accentColorString;} .tag {background-color: $accentColorString; color: $accentTextColor; } div{background-color: $cardBackgroundColor;}"
+ )
+ .replace("{link-color}", colorToCSS(accentColor()))
+ .replace(
+ "{link-color-active}",
+ colorToCSS(
+ lightenColor(accentColor())
+ )
+ )
+ binding.webView.loadData(changeLog, "text/html", "UTF-8")
+ binding.webView.webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?
+ ): Boolean {
+ val url = request?.url ?: return false
+ //you can do checks here e.g. url.host equals to target one
+ startActivity(Intent(Intent.ACTION_VIEW, url))
+ return true
+ }
+ }
+ } catch (e: Throwable) {
+ binding.webView.loadData(
+ "Unable to load " + e.localizedMessage + "
", "text/html", "UTF-8"
+ )
+ }
+ setChangelogRead(requireContext())
+ binding.tgFab.setOnClickListener {
+ openUrl(Constants.TELEGRAM_CHANGE_LOG)
+ }
+ binding.tgFab.accentColor()
+ binding.tgFab.shrink()
+ binding.container.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, oldScrollY: Int ->
+ val dy = scrollY - oldScrollY
+ if (dy > 0) {
+ binding.tgFab.shrink()
+ } else if (dy < 0) {
+ binding.tgFab.extend()
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ _binding = null
+ }
+
+ companion object {
+
+ const val TAG = "WhatsNewFragment"
+ private fun colorToCSS(color: Int): String {
+ 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 fun setChangelogRead(context: Context) {
+ try {
+ val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
+ val currentVersion = PackageInfoCompat.getLongVersionCode(pInfo)
+ lastVersion = currentVersion
+ } catch (e: PackageManager.NameNotFoundException) {
+ e.printStackTrace()
+ }
+ }
+
+ fun showChangeLog(activity: FragmentActivity) {
+ val pInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
+ val currentVersion = PackageInfoCompat.getLongVersionCode(pInfo)
+ if (currentVersion > lastVersion && !BuildConfig.DEBUG) {
+ val changelogBottomSheet = WhatsNewFragment()
+ changelogBottomSheet.show(activity.supportFragmentManager, TAG)
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..1e0c110d5
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsBaseActivity.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.Intent
+import android.content.pm.PackageManager
+import android.graphics.Rect
+import android.media.AudioManager
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import androidx.core.app.ActivityCompat
+import androidx.core.content.getSystemService
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.rootView
+import code.name.monkey.retromusic.util.logD
+import com.google.android.material.snackbar.Snackbar
+
+abstract class AbsBaseActivity : AbsThemeActivity() {
+ private var hadPermissions: Boolean = false
+ private lateinit var permissions: Array
+ private var permissionDeniedMessage: String? = null
+
+ open fun getPermissionsToRequest(): Array {
+ return arrayOf()
+ }
+
+ protected fun setPermissionDeniedMessage(message: String) {
+ permissionDeniedMessage = message
+ }
+
+ fun getPermissionDeniedMessage(): String {
+ return if (permissionDeniedMessage == null) getString(R.string.permissions_denied) else permissionDeniedMessage!!
+ }
+
+ private val snackBarContainer: View
+ get() = rootView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ volumeControlStream = AudioManager.STREAM_MUSIC
+ permissions = getPermissionsToRequest()
+ hadPermissions = hasPermissions()
+ permissionDeniedMessage = null
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val hasPermissions = hasPermissions()
+ if (hasPermissions != hadPermissions) {
+ hadPermissions = hasPermissions
+ if (VersionUtils.hasMarshmallow()) {
+ onHasPermissionsChanged(hasPermissions)
+ }
+ }
+ }
+
+ protected open fun onHasPermissionsChanged(hasPermissions: Boolean) {
+ // implemented by sub classes
+ logD(hasPermissions)
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ if (event.keyCode == KeyEvent.KEYCODE_MENU && event.action == KeyEvent.ACTION_UP) {
+ showOverflowMenu()
+ return true
+ }
+ return super.dispatchKeyEvent(event)
+ }
+
+ private fun showOverflowMenu() {
+ }
+
+ protected open fun requestPermissions() {
+ ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST)
+ }
+
+ protected fun hasPermissions(): Boolean {
+ for (permission in permissions) {
+ if (ActivityCompat.checkSelfPermission(this,
+ permission) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return false
+ }
+ }
+ return true
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == PERMISSION_REQUEST) {
+ for (grantResult in grantResults) {
+ if (grantResult != PackageManager.PERMISSION_GRANTED) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(
+ this@AbsBaseActivity, Manifest.permission.READ_EXTERNAL_STORAGE,
+ ) || ActivityCompat.shouldShowRequestPermissionRationale(
+ this@AbsBaseActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ )
+ ) {
+ // User has deny from permission dialog
+ Snackbar.make(
+ snackBarContainer,
+ permissionDeniedMessage!!,
+ Snackbar.LENGTH_SHORT
+ )
+ .setAction(R.string.action_grant) { requestPermissions() }
+ .setActionTextColor(accentColor()).show()
+ } else {
+ // User has deny permission and checked never show permission dialog so you can redirect to Application settings page
+ Snackbar.make(
+ snackBarContainer,
+ permissionDeniedMessage!!,
+ Snackbar.LENGTH_INDEFINITE
+ )
+ .setAction(R.string.action_settings) {
+ val intent = Intent()
+ intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ val uri = Uri.fromParts(
+ "package",
+ this@AbsBaseActivity.packageName,
+ null
+ )
+ intent.data = uri
+ startActivity(intent)
+ }.setActionTextColor(accentColor()).show()
+ }
+ return
+ }
+ }
+ hadPermissions = true
+ onHasPermissionsChanged(true)
+ } else if (requestCode == BLUETOOTH_PERMISSION_REQUEST) {
+ for (grantResult in grantResults) {
+ if (grantResult != PackageManager.PERMISSION_GRANTED) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(
+ this@AbsBaseActivity, Manifest.permission.BLUETOOTH_CONNECT
+ )
+ ) {
+ // User has deny from permission dialog
+ Snackbar.make(
+ snackBarContainer,
+ R.string.permission_bluetooth_denied,
+ Snackbar.LENGTH_SHORT
+ )
+ .setAction(R.string.action_grant) {
+ ActivityCompat.requestPermissions(this,
+ arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
+ BLUETOOTH_PERMISSION_REQUEST)
+ }
+ .setActionTextColor(accentColor()).show()
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val PERMISSION_REQUEST = 100
+ const val BLUETOOTH_PERMISSION_REQUEST = 101
+ }
+
+ // this lets keyboard close when clicked in background
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ val v = currentFocus
+ if (v is EditText) {
+ val outRect = Rect()
+ v.getGlobalVisibleRect(outRect)
+ if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
+ v.clearFocus()
+ getSystemService()?.hideSoftInputFromWindow(
+ v.windowToken,
+ 0
+ )
+ }
+ }
+ }
+ return super.dispatchTouchEvent(event)
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/ui/activities/base/AbsMusicServiceActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
similarity index 53%
rename from app/src/main/java/code/name/monkey/retromusic/ui/activities/base/AbsMusicServiceActivity.kt
rename to app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
index e22fb8013..f9f05c919 100644
--- a/app/src/main/java/code/name/monkey/retromusic/ui/activities/base/AbsMusicServiceActivity.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
@@ -1,26 +1,49 @@
-package code.name.monkey.retromusic.ui.activities.base
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.os.Bundle
import android.os.IBinder
-import code.name.monkey.retromusic.Constants.MEDIA_STORE_CHANGED
-import code.name.monkey.retromusic.Constants.META_CHANGED
-import code.name.monkey.retromusic.Constants.PLAY_STATE_CHANGED
-import code.name.monkey.retromusic.Constants.QUEUE_CHANGED
-import code.name.monkey.retromusic.Constants.REPEAT_MODE_CHANGED
-import code.name.monkey.retromusic.Constants.SHUFFLE_MODE_CHANGED
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.db.toPlayCount
import code.name.monkey.retromusic.helper.MusicPlayerRemote
-import code.name.monkey.retromusic.interfaces.MusicServiceEventListener
+import code.name.monkey.retromusic.interfaces.IMusicServiceEventListener
+import code.name.monkey.retromusic.repository.RealRepository
+import code.name.monkey.retromusic.service.MusicService.Companion.FAVORITE_STATE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.MEDIA_STORE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.META_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.PLAY_STATE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.QUEUE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.REPEAT_MODE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_CHANGED
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.logD
+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 {
-abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventListener {
-
- private val mMusicServiceEventListeners = ArrayList()
-
+ private val mMusicServiceEventListeners = ArrayList()
+ private val repository: RealRepository by inject()
private var serviceToken: MusicPlayerRemote.ServiceToken? = null
private var musicStateReceiver: MusicStateReceiver? = null
private var receiverRegistered: Boolean = false
@@ -37,7 +60,7 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
}
})
- setPermissionDeniedMessage(getString(R.string.permission_external_storage_denied));
+ setPermissionDeniedMessage(getString(R.string.permission_external_storage_denied))
}
override fun onDestroy() {
@@ -49,15 +72,15 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
}
}
- fun addMusicServiceEventListener(listener: MusicServiceEventListener?) {
- if (listener != null) {
- mMusicServiceEventListeners.add(listener)
+ fun addMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
+ if (listenerI != null) {
+ mMusicServiceEventListeners.add(listenerI)
}
}
- fun removeMusicServiceEventListener(listener: MusicServiceEventListener?) {
- if (listener != null) {
- mMusicServiceEventListeners.remove(listener)
+ fun removeMusicServiceEventListener(listenerI: IMusicServiceEventListener?) {
+ if (listenerI != null) {
+ mMusicServiceEventListeners.remove(listenerI)
}
}
@@ -72,9 +95,9 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
filter.addAction(META_CHANGED)
filter.addAction(QUEUE_CHANGED)
filter.addAction(MEDIA_STORE_CHANGED)
+ filter.addAction(FAVORITE_STATE_CHANGED)
- registerReceiver(musicStateReceiver, filter)
-
+ ContextCompat.registerReceiver(this, musicStateReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
receiverRegistered = true
}
@@ -98,6 +121,16 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
for (listener in mMusicServiceEventListeners) {
listener.onPlayingMetaChanged()
}
+ lifecycleScope.launch(Dispatchers.IO) {
+ if (!PreferenceUtil.pauseHistory) {
+ repository.upsertSongInHistory(MusicPlayerRemote.currentSong)
+ }
+ val song = repository.findSongExistInPlayCount(MusicPlayerRemote.currentSong.id)
+ ?.apply { playCount += 1 }
+ ?: MusicPlayerRemote.currentSong.toPlayCount()
+
+ repository.upsertSongInPlayCount(song)
+ }
}
override fun onQueueChanged() {
@@ -130,16 +163,35 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
}
}
+ override fun onFavoriteStateChanged() {
+ for (listener in mMusicServiceEventListeners) {
+ listener.onFavoriteStateChanged()
+ }
+ }
+
override fun onHasPermissionsChanged(hasPermissions: Boolean) {
super.onHasPermissionsChanged(hasPermissions)
val intent = Intent(MEDIA_STORE_CHANGED)
- intent.putExtra("from_permissions_changed", true) // just in case we need to know this at some point
+ intent.putExtra(
+ "from_permissions_changed",
+ true
+ ) // just in case we need to know this at some point
sendBroadcast(intent)
+ logD("sendBroadcast $hasPermissions")
}
-
override fun getPermissionsToRequest(): Array {
- return arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ return mutableListOf().apply {
+ if (VersionUtils.hasT()) {
+ add(Manifest.permission.READ_MEDIA_AUDIO)
+ add(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ add(Manifest.permission.READ_EXTERNAL_STORAGE)
+ }
+ if (!VersionUtils.hasR()) {
+ add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ }.toTypedArray()
}
private class MusicStateReceiver(activity: AbsMusicServiceActivity) : BroadcastReceiver() {
@@ -151,6 +203,7 @@ abstract class AbsMusicServiceActivity : AbsCastActivity(), MusicServiceEventLis
val activity = reference.get()
if (activity != null && action != null) {
when (action) {
+ FAVORITE_STATE_CHANGED -> activity.onFavoriteStateChanged()
META_CHANGED -> activity.onPlayingMetaChanged()
QUEUE_CHANGED -> activity.onQueueChanged()
PLAY_STATE_CHANGED -> activity.onPlayStateChanged()
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
new file mode 100644
index 000000000..2a81ca9b4
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsSlidingMusicPanelActivity.kt
@@ -0,0 +1,581 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.animation.PathInterpolator
+import android.widget.FrameLayout
+import androidx.activity.OnBackPressedCallback
+import androidx.core.animation.doOnEnd
+import androidx.core.view.*
+import androidx.fragment.app.commit
+import code.name.monkey.appthemehelper.util.VersionUtils
+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.CAROUSEL_EFFECT
+import code.name.monkey.retromusic.CIRCLE_PLAY_BUTTON
+import code.name.monkey.retromusic.EXTRA_SONG_INFO
+import code.name.monkey.retromusic.KEEP_SCREEN_ON
+import code.name.monkey.retromusic.LIBRARY_CATEGORIES
+import code.name.monkey.retromusic.NOW_PLAYING_SCREEN_ID
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.SCREEN_ON_LYRICS
+import code.name.monkey.retromusic.SWIPE_ANYWHERE_NOW_PLAYING
+import code.name.monkey.retromusic.SWIPE_DOWN_DISMISS
+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_VOLUME
+import code.name.monkey.retromusic.activities.PermissionActivity
+import code.name.monkey.retromusic.databinding.SlidingMusicPanelLayoutBinding
+import code.name.monkey.retromusic.extensions.*
+import code.name.monkey.retromusic.fragments.LibraryViewModel
+import code.name.monkey.retromusic.fragments.NowPlayingScreen
+import code.name.monkey.retromusic.fragments.NowPlayingScreen.*
+import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
+import code.name.monkey.retromusic.fragments.other.MiniPlayerFragment
+import code.name.monkey.retromusic.fragments.player.adaptive.AdaptiveFragment
+import code.name.monkey.retromusic.fragments.player.blur.BlurPlayerFragment
+import code.name.monkey.retromusic.fragments.player.card.CardFragment
+import code.name.monkey.retromusic.fragments.player.cardblur.CardBlurFragment
+import code.name.monkey.retromusic.fragments.player.circle.CirclePlayerFragment
+import code.name.monkey.retromusic.fragments.player.classic.ClassicPlayerFragment
+import code.name.monkey.retromusic.fragments.player.color.ColorFragment
+import code.name.monkey.retromusic.fragments.player.fit.FitFragment
+import code.name.monkey.retromusic.fragments.player.flat.FlatPlayerFragment
+import code.name.monkey.retromusic.fragments.player.full.FullPlayerFragment
+import code.name.monkey.retromusic.fragments.player.gradient.GradientPlayerFragment
+import code.name.monkey.retromusic.fragments.player.material.MaterialFragment
+import code.name.monkey.retromusic.fragments.player.md3.MD3PlayerFragment
+import code.name.monkey.retromusic.fragments.player.normal.PlayerFragment
+import code.name.monkey.retromusic.fragments.player.peek.PeekPlayerFragment
+import code.name.monkey.retromusic.fragments.player.plain.PlainPlayerFragment
+import code.name.monkey.retromusic.fragments.player.simple.SimplePlayerFragment
+import code.name.monkey.retromusic.fragments.player.tiny.TinyPlayerFragment
+import code.name.monkey.retromusic.fragments.queue.PlayingQueueFragment
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.model.CategoryInfo
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.ViewUtil
+import code.name.monkey.retromusic.util.logD
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_DRAGGING
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_SETTLING
+import com.google.android.material.bottomsheet.BottomSheetBehavior.from
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+
+abstract class AbsSlidingMusicPanelActivity : AbsMusicServiceActivity(),
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ companion object {
+ val TAG: String = AbsSlidingMusicPanelActivity::class.java.simpleName
+ }
+
+ var fromNotification = false
+ private var windowInsets: WindowInsetsCompat? = null
+ protected val libraryViewModel by viewModel()
+ private lateinit var bottomSheetBehavior: BottomSheetBehavior
+ private lateinit var playerFragment: AbsPlayerFragment
+ private var miniPlayerFragment: MiniPlayerFragment? = null
+ private var nowPlayingScreen: NowPlayingScreen? = null
+ private var taskColor: Int = 0
+ private var paletteColor: Int = Color.WHITE
+ private var navigationBarColor = 0
+
+ private val panelState: Int
+ get() = bottomSheetBehavior.state
+ private lateinit var binding: SlidingMusicPanelLayoutBinding
+ private var isInOneTabMode = false
+
+ private var navigationBarColorAnimator: ValueAnimator? = null
+ private val argbEvaluator: ArgbEvaluator = ArgbEvaluator()
+
+ private val onBackPressedCallback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ println("Handle back press ${bottomSheetBehavior.state}")
+ if (!handleBackPress()) {
+ remove()
+ onBackPressedDispatcher.onBackPressed()
+ }
+ }
+ }
+
+ private val bottomSheetCallbackList by lazy {
+ object : BottomSheetCallback() {
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ setMiniPlayerAlphaProgress(slideOffset)
+ navigationBarColorAnimator?.cancel()
+ setNavigationBarColorPreOreo(
+ argbEvaluator.evaluate(
+ slideOffset,
+ surfaceColor(),
+ navigationBarColor
+ ) as Int
+ )
+ }
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ onBackPressedCallback.isEnabled = newState == STATE_EXPANDED
+ when (newState) {
+ STATE_EXPANDED -> {
+ onPanelExpanded()
+ if (PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics) {
+ keepScreenOn(true)
+ }
+ }
+
+ STATE_COLLAPSED -> {
+ onPanelCollapsed()
+ if ((PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics) || !PreferenceUtil.isScreenOnEnabled) {
+ keepScreenOn(false)
+ }
+ }
+
+ STATE_SETTLING, STATE_DRAGGING -> {
+ if (fromNotification) {
+ binding.navigationView.bringToFront()
+ fromNotification = false
+ }
+ }
+
+ STATE_HIDDEN -> {
+ MusicPlayerRemote.clearQueue()
+ }
+
+ else -> {
+ logD("Do a flip")
+ }
+ }
+ }
+ }
+ }
+
+ fun getBottomSheetBehavior() = bottomSheetBehavior
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!hasPermissions()) {
+ startActivity(Intent(this, PermissionActivity::class.java))
+ finish()
+ }
+ binding = SlidingMusicPanelLayoutBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ binding.root.setOnApplyWindowInsetsListener { _, insets ->
+ windowInsets = WindowInsetsCompat.toWindowInsetsCompat(insets)
+ insets
+ }
+ chooseFragmentForTheme()
+ setupSlidingUpPanel()
+ setupBottomSheet()
+ updateColor()
+ if (!PreferenceUtil.materialYou) {
+ binding.slidingPanel.backgroundTintList = ColorStateList.valueOf(darkAccentColor())
+ navigationView.backgroundTintList = ColorStateList.valueOf(darkAccentColor())
+ }
+
+ navigationBarColor = surfaceColor()
+
+ onBackPressedDispatcher.addCallback(onBackPressedCallback)
+ }
+
+ private fun setupBottomSheet() {
+ bottomSheetBehavior = from(binding.slidingPanel)
+ bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallbackList)
+ bottomSheetBehavior.isHideable = PreferenceUtil.swipeDownToDismiss
+ bottomSheetBehavior.significantVelocityThreshold = 300
+ setMiniPlayerAlphaProgress(0F)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ PreferenceUtil.registerOnSharedPreferenceChangedListener(this)
+ if (nowPlayingScreen != PreferenceUtil.nowPlayingScreen) {
+ postRecreate()
+ }
+ if (bottomSheetBehavior.state == STATE_EXPANDED) {
+ setMiniPlayerAlphaProgress(1f)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallbackList)
+ PreferenceUtil.unregisterOnSharedPreferenceChangedListener(this)
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ when (key) {
+ SWIPE_DOWN_DISMISS -> {
+ bottomSheetBehavior.isHideable = PreferenceUtil.swipeDownToDismiss
+ }
+
+ TOGGLE_ADD_CONTROLS -> {
+ miniPlayerFragment?.setUpButtons()
+ }
+
+ NOW_PLAYING_SCREEN_ID -> {
+ chooseFragmentForTheme()
+ binding.slidingPanel.updateLayoutParams {
+ height = if (nowPlayingScreen != Peek) {
+ ViewGroup.LayoutParams.MATCH_PARENT
+ } else {
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ }
+ onServiceConnected()
+ }
+ }
+
+ ALBUM_COVER_TRANSFORM, CAROUSEL_EFFECT,
+ ALBUM_COVER_STYLE, TOGGLE_VOLUME, EXTRA_SONG_INFO, CIRCLE_PLAY_BUTTON,
+ -> {
+ chooseFragmentForTheme()
+ onServiceConnected()
+ }
+
+ SWIPE_ANYWHERE_NOW_PLAYING -> {
+ playerFragment.addSwipeDetector()
+ }
+
+ ADAPTIVE_COLOR_APP -> {
+ if (PreferenceUtil.nowPlayingScreen in listOf(Normal, Material, Flat)) {
+ chooseFragmentForTheme()
+ onServiceConnected()
+ }
+ }
+
+ LIBRARY_CATEGORIES -> {
+ updateTabs()
+ }
+
+ TAB_TEXT_MODE -> {
+ navigationView.labelVisibilityMode = PreferenceUtil.tabTitleMode
+ }
+
+ TOGGLE_FULL_SCREEN -> {
+ recreate()
+ }
+
+ SCREEN_ON_LYRICS -> {
+ keepScreenOn(bottomSheetBehavior.state == STATE_EXPANDED && PreferenceUtil.lyricsScreenOn && PreferenceUtil.showLyrics || PreferenceUtil.isScreenOnEnabled)
+ }
+
+ KEEP_SCREEN_ON -> {
+ maybeSetScreenOn()
+ }
+ }
+ }
+
+ fun collapsePanel() {
+ bottomSheetBehavior.state = STATE_COLLAPSED
+ }
+
+ fun expandPanel() {
+ bottomSheetBehavior.state = STATE_EXPANDED
+ }
+
+ private fun setMiniPlayerAlphaProgress(progress: Float) {
+ if (progress < 0) return
+ val alpha = 1 - progress
+ miniPlayerFragment?.view?.alpha = 1 - (progress / 0.2F)
+ miniPlayerFragment?.view?.isGone = alpha == 0f
+ if (!isLandscape) {
+ binding.navigationView.translationY = progress * 500
+ binding.navigationView.alpha = alpha
+ }
+ binding.playerFragmentContainer.alpha = (progress - 0.2F) / 0.2F
+ }
+
+ private fun animateNavigationBarColor(color: Int) {
+ if (VersionUtils.hasOreo()) return
+ navigationBarColorAnimator?.cancel()
+ navigationBarColorAnimator = ValueAnimator
+ .ofArgb(window.navigationBarColor, color).apply {
+ duration = ViewUtil.RETRO_MUSIC_ANIM_TIME.toLong()
+ interpolator = PathInterpolator(0.4f, 0f, 1f, 1f)
+ addUpdateListener { animation: ValueAnimator ->
+ setNavigationBarColorPreOreo(
+ animation.animatedValue as Int
+ )
+ }
+ start()
+ }
+ }
+
+ open fun onPanelCollapsed() {
+ setMiniPlayerAlphaProgress(0F)
+ // restore values
+ animateNavigationBarColor(surfaceColor())
+ setLightStatusBarAuto()
+ setLightNavigationBarAuto()
+ setTaskDescriptionColor(taskColor)
+ //playerFragment?.onHide()
+ }
+
+ open fun onPanelExpanded() {
+ setMiniPlayerAlphaProgress(1F)
+ onPaletteColorChanged()
+ //playerFragment?.onShow()
+ }
+
+ private fun setupSlidingUpPanel() {
+ binding.slidingPanel.viewTreeObserver.addOnGlobalLayoutListener(object :
+ ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ binding.slidingPanel.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ if (nowPlayingScreen != Peek) {
+ binding.slidingPanel.updateLayoutParams {
+ height = ViewGroup.LayoutParams.MATCH_PARENT
+ }
+ }
+ when (panelState) {
+ STATE_EXPANDED -> onPanelExpanded()
+ STATE_COLLAPSED -> onPanelCollapsed()
+ else -> {
+ // playerFragment!!.onHide()
+ }
+ }
+ }
+ })
+ }
+
+ val navigationView get() = binding.navigationView
+
+ val slidingPanel get() = binding.slidingPanel
+
+ val isBottomNavVisible get() = navigationView.isVisible && navigationView is BottomNavigationView
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ hideBottomSheet(false)
+ }
+
+ override fun onQueueChanged() {
+ super.onQueueChanged()
+ // Mini player should be hidden in Playing Queue
+ // it may pop up if hideBottomSheet is called
+ if (currentFragment(R.id.fragment_container) !is PlayingQueueFragment) {
+ hideBottomSheet(MusicPlayerRemote.playingQueue.isEmpty())
+ }
+ }
+
+ private fun handleBackPress(): Boolean {
+ if (panelState == STATE_EXPANDED) {
+ collapsePanel()
+ return true
+ }
+ return false
+ }
+
+ private fun onPaletteColorChanged() {
+ if (panelState == STATE_EXPANDED) {
+ navigationBarColor = surfaceColor()
+ setTaskDescColor(paletteColor)
+ val isColorLight = paletteColor.isColorLight
+ if (PreferenceUtil.isAdaptiveColor && (nowPlayingScreen == Normal || nowPlayingScreen == Flat || nowPlayingScreen == Material)) {
+ setLightNavigationBar(true)
+ setLightStatusBar(isColorLight)
+ } else if (nowPlayingScreen == Card || nowPlayingScreen == Blur || nowPlayingScreen == BlurCard) {
+ animateNavigationBarColor(Color.BLACK)
+ navigationBarColor = Color.BLACK
+ setLightStatusBar(false)
+ setLightNavigationBar(true)
+ } else if (nowPlayingScreen == Color || nowPlayingScreen == Tiny || nowPlayingScreen == Gradient) {
+ animateNavigationBarColor(paletteColor)
+ navigationBarColor = paletteColor
+ setLightNavigationBar(isColorLight)
+ setLightStatusBar(isColorLight)
+ } else if (nowPlayingScreen == Full) {
+ animateNavigationBarColor(paletteColor)
+ navigationBarColor = paletteColor
+ setLightNavigationBar(isColorLight)
+ setLightStatusBar(false)
+ } else if (nowPlayingScreen == Classic) {
+ setLightStatusBar(false)
+ } else if (nowPlayingScreen == Fit) {
+ setLightStatusBar(false)
+ }
+ }
+ }
+
+ private fun setTaskDescColor(color: Int) {
+ taskColor = color
+ if (panelState == STATE_COLLAPSED) {
+ setTaskDescriptionColor(color)
+ }
+ }
+
+ fun updateTabs() {
+ binding.navigationView.menu.clear()
+ val currentTabs: List = PreferenceUtil.libraryCategory
+ for (tab in currentTabs) {
+ if (tab.visible) {
+ val menu = tab.category
+ binding.navigationView.menu.add(0, menu.id, 0, menu.stringRes)
+ .setIcon(menu.icon)
+ }
+ }
+ if (binding.navigationView.menu.size() == 1) {
+ isInOneTabMode = true
+ binding.navigationView.isVisible = false
+ } else {
+ isInOneTabMode = false
+ }
+ }
+
+ private fun updateColor() {
+ libraryViewModel.paletteColor.observe(this) { color ->
+ this.paletteColor = color
+ onPaletteColorChanged()
+ }
+ }
+
+ fun setBottomNavVisibility(
+ visible: Boolean,
+ animate: Boolean = false,
+ hideBottomSheet: Boolean = MusicPlayerRemote.playingQueue.isEmpty(),
+ ) {
+ if (!ViewCompat.isLaidOut(navigationView)) {
+ return
+ }
+ if (isInOneTabMode) {
+ hideBottomSheet(
+ hide = hideBottomSheet,
+ animate = animate,
+ isBottomNavVisible = false
+ )
+ return
+ }
+ if (visible xor navigationView.isVisible) {
+ val mAnimate = animate && bottomSheetBehavior.state == STATE_COLLAPSED
+ if (mAnimate) {
+ if (visible) {
+ binding.navigationView.bringToFront()
+ binding.navigationView.show()
+ } else {
+ binding.navigationView.hide()
+ }
+ } else {
+ binding.navigationView.isVisible = visible
+ if (visible && bottomSheetBehavior.state != STATE_EXPANDED) {
+ binding.navigationView.bringToFront()
+ }
+ }
+ }
+ hideBottomSheet(
+ hide = hideBottomSheet,
+ animate = animate,
+ isBottomNavVisible = visible && navigationView is BottomNavigationView
+ )
+ }
+
+ fun hideBottomSheet(
+ hide: Boolean,
+ animate: Boolean = false,
+ isBottomNavVisible: Boolean = navigationView.isVisible && navigationView is BottomNavigationView,
+ ) {
+ val heightOfBar = windowInsets.getBottomInsets() + dip(R.dimen.mini_player_height)
+ val heightOfBarWithTabs = heightOfBar + dip(R.dimen.bottom_nav_height)
+ if (hide) {
+ bottomSheetBehavior.peekHeight = -windowInsets.getBottomInsets()
+ bottomSheetBehavior.state = STATE_COLLAPSED
+ libraryViewModel.setFabMargin(
+ this,
+ if (isBottomNavVisible) dip(R.dimen.bottom_nav_height) else 0
+ )
+ } else {
+ if (MusicPlayerRemote.playingQueue.isNotEmpty()) {
+ binding.slidingPanel.elevation = 0F
+ binding.navigationView.elevation = 5F
+ if (isBottomNavVisible) {
+ logD("List")
+ if (animate) {
+ bottomSheetBehavior.peekHeightAnimate(heightOfBarWithTabs)
+ } else {
+ bottomSheetBehavior.peekHeight = heightOfBarWithTabs
+ }
+ libraryViewModel.setFabMargin(
+ this,
+ dip(R.dimen.bottom_nav_mini_player_height)
+ )
+ } else {
+ logD("Details")
+ if (animate) {
+ bottomSheetBehavior.peekHeightAnimate(heightOfBar).doOnEnd {
+ binding.slidingPanel.bringToFront()
+ }
+ } else {
+ bottomSheetBehavior.peekHeight = heightOfBar
+ binding.slidingPanel.bringToFront()
+ }
+ libraryViewModel.setFabMargin(this, dip(R.dimen.mini_player_height))
+ }
+ }
+ }
+ }
+
+ fun setAllowDragging(allowDragging: Boolean) {
+ bottomSheetBehavior.isDraggable = allowDragging
+ hideBottomSheet(false)
+ }
+
+ private fun chooseFragmentForTheme() {
+ nowPlayingScreen = PreferenceUtil.nowPlayingScreen
+
+ val fragment: AbsPlayerFragment = when (nowPlayingScreen) {
+ Blur -> BlurPlayerFragment()
+ Adaptive -> AdaptiveFragment()
+ Normal -> PlayerFragment()
+ Card -> CardFragment()
+ BlurCard -> CardBlurFragment()
+ Fit -> FitFragment()
+ Flat -> FlatPlayerFragment()
+ Full -> FullPlayerFragment()
+ Plain -> PlainPlayerFragment()
+ Simple -> SimplePlayerFragment()
+ Material -> MaterialFragment()
+ Color -> ColorFragment()
+ Gradient -> GradientPlayerFragment()
+ Tiny -> TinyPlayerFragment()
+ Peek -> PeekPlayerFragment()
+ Circle -> CirclePlayerFragment()
+ Classic -> ClassicPlayerFragment()
+ MD3 -> MD3PlayerFragment()
+ else -> PlayerFragment()
+ } // must extend AbsPlayerFragment
+ supportFragmentManager.commit {
+ replace(R.id.playerFragmentContainer, fragment)
+ }
+ supportFragmentManager.executePendingTransactions()
+ playerFragment = whichFragment(R.id.playerFragmentContainer)
+ miniPlayerFragment = whichFragment(R.id.miniPlayerFragment)
+ miniPlayerFragment?.view?.setOnClickListener { expandPanel() }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt
new file mode 100644
index 000000000..32b025f01
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsThemeActivity.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.KeyEvent
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
+import androidx.core.os.LocaleListCompat
+import code.name.monkey.appthemehelper.common.ATHToolbarActivity
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.*
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.theme.getNightMode
+import code.name.monkey.retromusic.util.theme.getThemeResValue
+
+abstract class AbsThemeActivity : ATHToolbarActivity(), Runnable {
+
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ updateLocale()
+ updateTheme()
+ hideStatusBar()
+ super.onCreate(savedInstanceState)
+ setEdgeToEdgeOrImmersive()
+ maybeSetScreenOn()
+ setLightNavigationBarAuto()
+ setLightStatusBarAuto(surfaceColor())
+ if (VersionUtils.hasQ()) {
+ window.decorView.isForceDarkAllowed = false
+ }
+ }
+
+ private fun updateTheme() {
+ setTheme(getThemeResValue())
+ if (PreferenceUtil.materialYou) {
+ setDefaultNightMode(getNightMode())
+ }
+
+ if (PreferenceUtil.isCustomFont) {
+ setTheme(R.style.FontThemeOverlay)
+ }
+ }
+
+ private fun updateLocale() {
+ val localeCode = PreferenceUtil.languageCode
+ if (PreferenceUtil.isLocaleAutoStorageEnabled) {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(localeCode))
+ PreferenceUtil.isLocaleAutoStorageEnabled = true
+ }
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (hasFocus) {
+ hideStatusBar()
+ handler.removeCallbacks(this)
+ handler.postDelayed(this, 300)
+ } else {
+ handler.removeCallbacks(this)
+ }
+ }
+
+ override fun run() {
+ setImmersiveFullscreen()
+ }
+
+ override fun onStop() {
+ handler.removeCallbacks(this)
+ super.onStop()
+ }
+
+ public override fun onDestroy() {
+ super.onDestroy()
+ exitFullscreen()
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ handler.removeCallbacks(this)
+ handler.postDelayed(this, 500)
+ }
+ return super.onKeyDown(keyCode, event)
+ }
+
+ override fun attachBaseContext(newBase: Context?) {
+ super.attachBaseContext(newBase)
+ }
+}
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
new file mode 100644
index 000000000..e99881953
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/BugReportActivity.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.content.ClipData
+import android.content.ClipboardManager
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import code.name.monkey.appthemehelper.util.TintHelper
+import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsThemeActivity
+import code.name.monkey.retromusic.activities.bugreport.model.DeviceInfo
+import code.name.monkey.retromusic.databinding.ActivityBugReportBinding
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
+import code.name.monkey.retromusic.extensions.showToast
+
+open class BugReportActivity : AbsThemeActivity() {
+
+ private lateinit var binding: ActivityBugReportBinding
+ private var deviceInfo: DeviceInfo? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityBugReportBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setTaskDescriptionColorAuto()
+
+ initViews()
+
+ if (title.isNullOrEmpty()) setTitle(R.string.report_an_issue)
+
+ deviceInfo = DeviceInfo(this)
+ binding.cardDeviceInfo.airTextDeviceInfo.text = deviceInfo.toString()
+ }
+
+ private fun initViews() {
+ val accentColor = accentColor()
+ setSupportActionBar(binding.toolbar)
+ ToolbarContentTintHelper.colorBackButton(binding.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ binding.cardDeviceInfo.airTextDeviceInfo.setOnClickListener { copyDeviceInfoToClipBoard() }
+
+ TintHelper.setTintAuto(binding.sendFab, accentColor, true)
+ binding.sendFab.setOnClickListener { reportIssue() }
+ }
+
+ private fun reportIssue() {
+ copyDeviceInfoToClipBoard()
+ val i = Intent(Intent.ACTION_VIEW)
+ i.data = ISSUE_TRACKER_LINK.toUri()
+ i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ startActivity(i)
+ }
+
+ private fun copyDeviceInfoToClipBoard() {
+ val clipboard = getSystemService()
+ val clip = ClipData.newPlainText(getString(R.string.device_info), deviceInfo?.toMarkdown())
+ clipboard?.setPrimaryClip(clip)
+ showToast(R.string.copied_device_info_to_clipboard)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ companion object {
+ private const val ISSUE_TRACKER_LINK =
+ "https://github.com/MuntashirAkon/Metro/issues/new"
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.kt b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.kt
new file mode 100644
index 000000000..19aff73a2
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/bugreport/model/DeviceInfo.kt
@@ -0,0 +1,111 @@
+package code.name.monkey.retromusic.activities.bugreport.model
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.IntRange
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.content.pm.PackageInfoCompat
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.PreferenceUtil.isAdaptiveColor
+import code.name.monkey.retromusic.util.PreferenceUtil.nowPlayingScreen
+import java.util.*
+
+class DeviceInfo(context: Context) {
+ @SuppressLint("NewApi")
+ private val abis = Build.SUPPORTED_ABIS
+
+ @SuppressLint("NewApi")
+ private val abis32Bits = Build.SUPPORTED_32_BIT_ABIS
+
+ @SuppressLint("NewApi")
+ private val abis64Bits = Build.SUPPORTED_64_BIT_ABIS
+ private val baseTheme: String
+ private val brand = Build.BRAND
+ private val buildID = Build.DISPLAY
+ private val buildVersion = Build.VERSION.INCREMENTAL
+ private val device = Build.DEVICE
+ private val hardware = Build.HARDWARE
+ private val isAdaptive: Boolean
+ private val manufacturer = Build.MANUFACTURER
+ private val model = Build.MODEL
+ private val nowPlayingTheme: String
+ private val product = Build.PRODUCT
+ private val releaseVersion = Build.VERSION.RELEASE
+
+ @IntRange(from = 0)
+ private val sdkVersion = Build.VERSION.SDK_INT
+ private var versionCode = 0L
+ private var versionName: String? = null
+ private val selectedLang: String
+ fun toMarkdown(): String {
+ return """
+ Device info:
+ ---
+
+ 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
+
+
+ """.trimIndent()
+ }
+
+ override fun toString(): String {
+ return """
+ 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)}
+ Base theme: $baseTheme
+ Now playing theme: $nowPlayingTheme
+ Adaptive: $isAdaptive
+ System language: ${Locale.getDefault().toLanguageTag()}
+ In-App Language: $selectedLang
+ """.trimIndent()
+ }
+
+ init {
+ val packageInfo = try {
+ context.packageManager.getPackageInfo(context.packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ null
+ }
+ if (packageInfo != null) {
+ versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
+ versionName = packageInfo.versionName
+ } else {
+ versionCode = -1
+ versionName = null
+ }
+ baseTheme = PreferenceUtil.baseTheme
+ nowPlayingTheme = context.getString(nowPlayingScreen.titleRes)
+ isAdaptive = isAdaptiveColor
+ selectedLang = AppCompatDelegate.getApplicationLocales().toLanguageTags()
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..efd56210e
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFGuideActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+
+package code.name.monkey.retromusic.activities.saf;
+
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+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. */
+public class SAFGuideActivity extends IntroActivity {
+
+ public static final int REQUEST_CODE_SAF_GUIDE = 98;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setButtonCtaVisible(false);
+ setButtonNextVisible(false);
+ setButtonBackVisible(false);
+
+ setButtonCtaTintMode(BUTTON_CTA_TINT_MODE_TEXT);
+
+ 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(code.name.monkey.appthemehelper.R.color.md_deep_purple_300)
+ .backgroundDark(code.name.monkey.appthemehelper.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(code.name.monkey.appthemehelper.R.color.md_deep_purple_500)
+ .backgroundDark(code.name.monkey.appthemehelper.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(code.name.monkey.appthemehelper.R.color.md_deep_purple_700)
+ .backgroundDark(code.name.monkey.appthemehelper.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/saf/SAFRequestActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFRequestActivity.kt
new file mode 100644
index 000000000..1b389b9d2
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/saf/SAFRequestActivity.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 Bartlomiej Uliasz.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.saf
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE
+import code.name.monkey.retromusic.util.SAFUtil
+
+/** Created by buliasz on 2021-02-07. */
+class SAFRequestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val intent = Intent(this, code.name.monkey.retromusic.activities.saf.SAFGuideActivity::class.java)
+ startActivityForResult(intent, REQUEST_CODE_SAF_GUIDE)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+ super.onActivityResult(requestCode, resultCode, intent)
+ when (requestCode) {
+ REQUEST_CODE_SAF_GUIDE -> {
+ SAFUtil.openTreePicker(this)
+ }
+ SAFUtil.REQUEST_SAF_PICK_TREE -> {
+ if (resultCode == RESULT_OK) {
+ SAFUtil.saveTreeUri(this, intent)
+ }
+ finish()
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100755
index 000000000..3a9ca4e4b
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AbsTagEditorActivity.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.app.SearchManager
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Bundle
+import android.provider.MediaStore
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.animation.OvershootInterpolator
+import android.widget.ImageView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import androidx.viewbinding.ViewBinding
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.base.AbsBaseActivity
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.colorButtons
+import code.name.monkey.retromusic.extensions.hideSoftKeyboard
+import code.name.monkey.retromusic.extensions.setTaskDescriptionColorAuto
+import code.name.monkey.retromusic.model.ArtworkInfo
+import code.name.monkey.retromusic.model.AudioTagInfo
+import code.name.monkey.retromusic.repository.Repository
+import code.name.monkey.retromusic.util.logD
+import code.name.monkey.retromusic.util.logE
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+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
+
+abstract class AbsTagEditorActivity : AbsBaseActivity() {
+ abstract val editorImage: ImageView
+ val repository by inject()
+
+ lateinit var saveFab: MaterialButton
+ protected var id: Long = 0
+ private set
+ private var paletteColorPrimary: Int = 0
+ private var songPaths: List? = null
+ private var savedSongPaths: List? = null
+ private val currentSongPath: String? = null
+ private var savedTags: Map? = null
+ private var savedArtworkInfo: ArtworkInfo? = null
+ private var _binding: VB? = null
+ protected val binding: VB get() = _binding!!
+ private var cacheFiles = listOf()
+
+ abstract val bindingInflater: (LayoutInflater) -> VB
+
+ private lateinit var launcher: ActivityResultLauncher
+
+ protected abstract fun loadImageFromFile(selectedFile: Uri?)
+
+ protected val show: AlertDialog
+ get() =
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.update_image)
+ .setItems(items.toTypedArray()) { _, position ->
+ when (position) {
+ 0 -> startImagePicker()
+ 1 -> searchImageOnWeb()
+ 2 -> deleteImage()
+ }
+ }
+ .setNegativeButton(R.string.action_cancel, null)
+ .show()
+ .colorButtons()
+
+ internal val albumArtist: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM_ARTIST)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val songTitle: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.TITLE)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+ protected val composer: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.COMPOSER)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val albumTitle: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val artistName: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ARTIST)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val albumArtistName: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.ALBUM_ARTIST)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val genreName: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.GENRE)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val songYear: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.YEAR)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val trackNumber: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.TRACK)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val discNumber: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.DISC_NO)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val lyrics: String?
+ get() {
+ return try {
+ getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.getFirst(FieldKey.LYRICS)
+ } catch (e: Exception) {
+ logE(e)
+ null
+ }
+ }
+
+ protected val albumArt: Bitmap?
+ get() {
+ try {
+ val artworkTag = getAudioFile(songPaths!![0]).tagOrCreateAndSetDefault.firstArtwork
+ if (artworkTag != null) {
+ val artworkBinaryData = artworkTag.binaryData
+ return BitmapFactory.decodeByteArray(
+ artworkBinaryData,
+ 0,
+ artworkBinaryData.size
+ )
+ }
+ return null
+ } catch (e: Exception) {
+ logE(e)
+ return null
+ }
+ }
+
+ private val pickArtworkImage =
+ registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
+ loadImageFromFile(uri)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ _binding = bindingInflater.invoke(layoutInflater)
+ setContentView(binding.root)
+ setTaskDescriptionColorAuto()
+
+ saveFab = findViewById(R.id.saveTags)
+ getIntentExtras()
+
+ songPaths = getSongPaths()
+ logD(songPaths?.size)
+ if (songPaths!!.isEmpty()) {
+ finish()
+ }
+ setUpViews()
+ launcher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ writeToFiles(getSongUris(), cacheFiles)
+ }
+ }
+ }
+
+ private fun setUpViews() {
+ setUpFab()
+ setUpImageView()
+ }
+
+ private lateinit var items: List
+
+ private fun setUpImageView() {
+ loadCurrentImage()
+ items = listOf(
+ getString(R.string.pick_from_local_storage),
+ getString(R.string.web_search),
+ getString(R.string.remove_cover)
+ )
+ editorImage.setOnClickListener { show }
+ }
+
+ private fun startImagePicker() {
+ pickArtworkImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
+ }
+
+ protected abstract fun loadCurrentImage()
+
+ protected abstract fun searchImageOnWeb()
+
+ protected abstract fun deleteImage()
+
+ private fun setUpFab() {
+ saveFab.accentColor()
+ saveFab.apply {
+ scaleX = 0f
+ scaleY = 0f
+ isEnabled = false
+ setOnClickListener { save() }
+ }
+ }
+
+ protected abstract fun save()
+
+ private fun getIntentExtras() {
+ val intentExtras = intent.extras
+ if (intentExtras != null) {
+ id = intentExtras.getLong(EXTRA_ID)
+ }
+ }
+
+ protected abstract fun getSongPaths(): List
+
+ protected abstract fun getSongUris(): List
+
+ protected fun searchWebFor(vararg keys: String) {
+ val stringBuilder = StringBuilder()
+ for (key in keys) {
+ stringBuilder.append(key)
+ stringBuilder.append(" ")
+ }
+ val intent = Intent(Intent.ACTION_WEB_SEARCH)
+ intent.putExtra(SearchManager.QUERY, stringBuilder.toString())
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ startActivity(intent)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressedDispatcher.onBackPressed()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ protected fun dataChanged() {
+ showFab()
+ }
+
+ private fun showFab() {
+ saveFab.animate().setDuration(500).setInterpolator(OvershootInterpolator()).scaleX(1f)
+ .scaleY(1f).start()
+ saveFab.isEnabled = true
+ }
+
+ private fun hideFab() {
+ saveFab.animate().setDuration(500).setInterpolator(OvershootInterpolator()).scaleX(0.0f)
+ .scaleY(0.0f).start()
+ saveFab.isEnabled = false
+ }
+
+ protected fun setImageBitmap(bitmap: Bitmap?, bgColor: Int) {
+ if (bitmap == null) {
+ editorImage.setImageResource(R.drawable.default_audio_art)
+ } else {
+ editorImage.setImageBitmap(bitmap)
+ }
+ setColors(bgColor)
+ }
+
+ protected open fun setColors(color: Int) {
+ paletteColorPrimary = color
+ }
+
+ protected fun writeValuesToFiles(
+ fieldKeyValueMap: Map,
+ artworkInfo: ArtworkInfo?
+ ) {
+ hideSoftKeyboard()
+
+ hideFab()
+ logD(fieldKeyValueMap)
+ GlobalScope.launch {
+ if (VersionUtils.hasR()) {
+ cacheFiles = TagWriter.writeTagsToFilesR(
+ this@AbsTagEditorActivity, AudioTagInfo(
+ songPaths,
+ fieldKeyValueMap,
+ artworkInfo
+ )
+ )
+
+ if (cacheFiles.isNotEmpty()) {
+ val pendingIntent =
+ MediaStore.createWriteRequest(contentResolver, getSongUris())
+ launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
+ }
+ } else {
+ TagWriter.writeTagsToFiles(
+ this@AbsTagEditorActivity, AudioTagInfo(
+ songPaths,
+ fieldKeyValueMap,
+ artworkInfo
+ )
+ )
+ }
+ }
+ }
+
+ private fun writeTags(paths: List?) {
+ GlobalScope.launch {
+ if (VersionUtils.hasR()) {
+ cacheFiles = TagWriter.writeTagsToFilesR(
+ this@AbsTagEditorActivity, AudioTagInfo(
+ paths,
+ savedTags,
+ savedArtworkInfo
+ )
+ )
+ val pendingIntent = MediaStore.createWriteRequest(contentResolver, getSongUris())
+
+ launcher.launch(IntentSenderRequest.Builder(pendingIntent).build())
+ } else {
+ TagWriter.writeTagsToFiles(
+ this@AbsTagEditorActivity, AudioTagInfo(
+ paths,
+ savedTags,
+ savedArtworkInfo
+ )
+ )
+ }
+ }
+ }
+
+ private lateinit var audioFile: AudioFile
+
+ private fun getAudioFile(path: String): AudioFile {
+ return try {
+ if (!this::audioFile.isInitialized) {
+ audioFile = AudioFileIO.read(File(path))
+ }
+ audioFile
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not read audio file $path", e)
+ AudioFile()
+ }
+ }
+
+ private fun writeToFiles(songUris: List, cacheFiles: List) {
+ if (cacheFiles.size == songUris.size) {
+ for (i in cacheFiles.indices) {
+ contentResolver.openOutputStream(songUris[i])?.use { output ->
+ cacheFiles[i].inputStream().use { input ->
+ input.copyTo(output)
+ }
+ }
+ }
+ }
+ lifecycleScope.launch {
+ TagWriter.scan(this@AbsTagEditorActivity, getSongPaths())
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ // Delete Cache Files
+ cacheFiles.forEach { file ->
+ file.delete()
+ }
+ }
+
+ companion object {
+ const val EXTRA_ID = "extra_id"
+ const val EXTRA_PALETTE = "extra_palette"
+ 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
new file mode 100755
index 000000000..2c9013657
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.transition.Slide
+import android.view.LayoutInflater
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.core.widget.doAfterTextChanged
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.ActivityAlbumTagEditorBinding
+import code.name.monkey.retromusic.extensions.*
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.model.ArtworkInfo
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.ImageUtil
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.RetroColorUtil.generatePalette
+import code.name.monkey.retromusic.util.RetroColorUtil.getColor
+import code.name.monkey.retromusic.util.logD
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.request.target.ImageViewTarget
+import com.bumptech.glide.request.transition.Transition
+import com.google.android.material.shape.MaterialShapeDrawable
+import org.jaudiotagger.tag.FieldKey
+import java.util.*
+
+class AlbumTagEditorActivity : AbsTagEditorActivity() {
+
+ override val bindingInflater: (LayoutInflater) -> ActivityAlbumTagEditorBinding =
+ ActivityAlbumTagEditorBinding::inflate
+
+ private fun windowEnterTransition() {
+ val slide = Slide()
+ slide.excludeTarget(R.id.appBarLayout, true)
+ slide.excludeTarget(R.id.status_bar, true)
+ slide.excludeTarget(android.R.id.statusBarBackground, true)
+ slide.excludeTarget(android.R.id.navigationBarBackground, true)
+
+ window.enterTransition = slide
+ }
+
+ private var albumArtBitmap: Bitmap? = null
+ private var deleteAlbumArt: Boolean = false
+
+ private fun setupToolbar() {
+ setSupportActionBar(binding.toolbar)
+ binding.appBarLayout?.statusBarForeground =
+ MaterialShapeDrawable.createWithElevationOverlay(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.sharedElementsUseOverlay = true
+ binding.imageContainer.transitionName = getString(R.string.transition_album_art)
+ windowEnterTransition()
+ setUpViews()
+ setupToolbar()
+ }
+
+ private fun setUpViews() {
+ fillViewsWithFileTags()
+
+ binding.yearContainer.setTint(false)
+ binding.genreContainer.setTint(false)
+ binding.albumTitleContainer.setTint(false)
+ binding.albumArtistContainer.setTint(false)
+
+ binding.albumText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.albumArtistText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.genreTitle.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.yearTitle.appHandleColor().doAfterTextChanged { dataChanged() }
+ }
+
+ private fun fillViewsWithFileTags() {
+ binding.albumText.setText(albumTitle)
+ binding.albumArtistText.setText(albumArtistName)
+ binding.genreTitle.setText(genreName)
+ binding.yearTitle.setText(songYear)
+ logD(albumTitle + albumArtistName)
+ }
+
+ override fun loadCurrentImage() {
+ val bitmap = albumArt
+ setImageBitmap(
+ bitmap,
+ getColor(
+ generatePalette(bitmap),
+ defaultFooterColor()
+ )
+ )
+ deleteAlbumArt = false
+ }
+
+ private fun toastLoadingFailed() {
+ showToast(R.string.could_not_download_album_cover)
+ }
+
+ override fun searchImageOnWeb() {
+ searchWebFor(binding.albumText.text.toString(), binding.albumArtistText.text.toString())
+ }
+
+ override fun deleteImage() {
+ setImageBitmap(
+ BitmapFactory.decodeResource(resources, R.drawable.default_audio_art),
+ defaultFooterColor()
+ )
+ deleteAlbumArt = true
+ dataChanged()
+ }
+
+ override fun loadImageFromFile(selectedFile: Uri?) {
+ Glide.with(this@AlbumTagEditorActivity)
+ .asBitmapPalette()
+ .load(selectedFile)
+ .diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true)
+ .into(object : ImageViewTarget(binding.editorImage) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?
+ ) {
+ getColor(resource.palette, Color.TRANSPARENT)
+ albumArtBitmap = resource.bitmap?.let { ImageUtil.resizeBitmap(it, 2048) }
+ setImageBitmap(
+ albumArtBitmap,
+ getColor(
+ resource.palette,
+ defaultFooterColor()
+ )
+ )
+ deleteAlbumArt = false
+ dataChanged()
+ setResult(Activity.RESULT_OK)
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ showToast(R.string.error_load_failed, Toast.LENGTH_LONG)
+ }
+
+ override fun setResource(resource: BitmapPaletteWrapper?) {}
+ })
+ }
+
+ override fun save() {
+ val fieldKeyValueMap = EnumMap(FieldKey::class.java)
+ fieldKeyValueMap[FieldKey.ALBUM] = binding.albumText.text.toString()
+ // android seems not to recognize album_artist field so we additionally write the normal artist field
+ fieldKeyValueMap[FieldKey.ARTIST] = binding.albumArtistText.text.toString()
+ fieldKeyValueMap[FieldKey.ALBUM_ARTIST] = binding.albumArtistText.text.toString()
+ fieldKeyValueMap[FieldKey.GENRE] = binding.genreTitle.text.toString()
+ fieldKeyValueMap[FieldKey.YEAR] = binding.yearTitle.text.toString()
+
+ writeValuesToFiles(
+ fieldKeyValueMap,
+ when {
+ deleteAlbumArt -> ArtworkInfo(id, null)
+ albumArtBitmap == null -> null
+ else -> ArtworkInfo(id, albumArtBitmap!!)
+ }
+ )
+ }
+
+ override fun getSongPaths(): List {
+ return repository.albumById(id).songs
+ .map(Song::data)
+ }
+
+ override fun getSongUris(): List = repository.albumById(id).songs.map {
+ MusicUtil.getSongFileUri(it.id)
+ }
+
+ override fun setColors(color: Int) {
+ super.setColors(color)
+ saveFab.backgroundTintList = ColorStateList.valueOf(color)
+ saveFab.backgroundTintList = ColorStateList.valueOf(color)
+ ColorStateList.valueOf(
+ MaterialValueHelper.getPrimaryTextColor(
+ this,
+ color.isColorLight
+ )
+ ).also {
+ saveFab.iconTint = it
+ saveFab.setTextColor(it)
+ }
+ }
+
+
+ override val editorImage: ImageView
+ get() = binding.editorImage
+
+ companion object {
+
+ val TAG: String = AlbumTagEditorActivity::class.java.simpleName
+ }
+}
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
new file mode 100755
index 000000000..93dd8a44f
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.annotation.SuppressLint
+import android.app.Activity
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.core.widget.doAfterTextChanged
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.ActivitySongTagEditorBinding
+import code.name.monkey.retromusic.extensions.*
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.model.ArtworkInfo
+import code.name.monkey.retromusic.repository.SongRepository
+import code.name.monkey.retromusic.util.ImageUtil
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.RetroColorUtil
+import code.name.monkey.retromusic.util.logD
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.request.target.ImageViewTarget
+import com.bumptech.glide.request.transition.Transition
+import com.google.android.material.shape.MaterialShapeDrawable
+import org.jaudiotagger.tag.FieldKey
+import org.koin.android.ext.android.inject
+import java.util.*
+
+class SongTagEditorActivity : AbsTagEditorActivity() {
+
+ override val bindingInflater: (LayoutInflater) -> ActivitySongTagEditorBinding =
+ ActivitySongTagEditorBinding::inflate
+
+
+ private val songRepository by inject()
+
+ private var albumArtBitmap: Bitmap? = null
+ private var deleteAlbumArt: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setUpViews()
+ setSupportActionBar(binding.toolbar)
+ binding.appBarLayout?.statusBarForeground =
+ MaterialShapeDrawable.createWithElevationOverlay(this)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun setUpViews() {
+ fillViewsWithFileTags()
+ binding.songTextContainer.setTint(false)
+ binding.composerContainer.setTint(false)
+ binding.albumTextContainer.setTint(false)
+ binding.artistContainer.setTint(false)
+ binding.albumArtistContainer.setTint(false)
+ binding.yearContainer.setTint(false)
+ binding.genreContainer.setTint(false)
+ binding.trackNumberContainer.setTint(false)
+ binding.discNumberContainer.setTint(false)
+ binding.lyricsContainer.setTint(false)
+
+ binding.songText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.albumText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.albumArtistText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.artistText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.genreText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.yearText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.trackNumberText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.discNumberText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.lyricsText.appHandleColor().doAfterTextChanged { dataChanged() }
+ binding.songComposerText.appHandleColor().doAfterTextChanged { dataChanged() }
+ }
+
+ private fun fillViewsWithFileTags() {
+ binding.songText.setText(songTitle)
+ binding.albumArtistText.setText(albumArtist)
+ binding.albumText.setText(albumTitle)
+ binding.artistText.setText(artistName)
+ binding.genreText.setText(genreName)
+ binding.yearText.setText(songYear)
+ binding.trackNumberText.setText(trackNumber)
+ binding.discNumberText.setText(discNumber)
+ binding.lyricsText.setText(lyrics)
+ binding.songComposerText.setText(composer)
+ logD(songTitle + songYear)
+ }
+
+ override fun loadCurrentImage() {
+ val bitmap = albumArt
+ setImageBitmap(
+ bitmap,
+ RetroColorUtil.getColor(
+ RetroColorUtil.generatePalette(bitmap),
+ defaultFooterColor()
+ )
+ )
+ deleteAlbumArt = false
+ }
+
+ override fun searchImageOnWeb() {
+ searchWebFor(binding.songText.text.toString(), binding.artistText.text.toString())
+ }
+
+ override fun deleteImage() {
+ setImageBitmap(
+ BitmapFactory.decodeResource(resources, R.drawable.default_audio_art),
+ defaultFooterColor()
+ )
+ deleteAlbumArt = true
+ dataChanged()
+ }
+
+ override fun setColors(color: Int) {
+ super.setColors(color)
+ saveFab.backgroundTintList = ColorStateList.valueOf(color)
+ ColorStateList.valueOf(
+ MaterialValueHelper.getPrimaryTextColor(
+ this,
+ color.isColorLight
+ )
+ ).also {
+ saveFab.iconTint = it
+ saveFab.setTextColor(it)
+ }
+ }
+
+ override fun save() {
+ val fieldKeyValueMap = EnumMap(FieldKey::class.java)
+ fieldKeyValueMap[FieldKey.TITLE] = binding.songText.text.toString()
+ fieldKeyValueMap[FieldKey.ALBUM] = binding.albumText.text.toString()
+ fieldKeyValueMap[FieldKey.ARTIST] = binding.artistText.text.toString()
+ fieldKeyValueMap[FieldKey.GENRE] = binding.genreText.text.toString()
+ fieldKeyValueMap[FieldKey.YEAR] = binding.yearText.text.toString()
+ fieldKeyValueMap[FieldKey.TRACK] = binding.trackNumberText.text.toString()
+ fieldKeyValueMap[FieldKey.DISC_NO] = binding.discNumberText.text.toString()
+ fieldKeyValueMap[FieldKey.LYRICS] = binding.lyricsText.text.toString()
+ fieldKeyValueMap[FieldKey.ALBUM_ARTIST] = binding.albumArtistText.text.toString()
+ fieldKeyValueMap[FieldKey.COMPOSER] = binding.songComposerText.text.toString()
+ writeValuesToFiles(
+ fieldKeyValueMap, when {
+ deleteAlbumArt -> ArtworkInfo(id, null)
+ albumArtBitmap == null -> null
+ else -> ArtworkInfo(id, albumArtBitmap!!)
+ }
+ )
+ }
+
+ override fun getSongPaths(): List = listOf(songRepository.song(id).data)
+
+ override fun getSongUris(): List = listOf(MusicUtil.getSongFileUri(id))
+
+ override fun loadImageFromFile(selectedFile: Uri?) {
+ Glide.with(this@SongTagEditorActivity)
+ .asBitmapPalette()
+ .load(selectedFile)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .skipMemoryCache(true)
+ .into(object : ImageViewTarget(binding.editorImage) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?
+ ) {
+ RetroColorUtil.getColor(resource.palette, Color.TRANSPARENT)
+ albumArtBitmap = resource.bitmap?.let { ImageUtil.resizeBitmap(it, 2048) }
+ setImageBitmap(
+ albumArtBitmap,
+ RetroColorUtil.getColor(
+ resource.palette,
+ defaultFooterColor()
+ )
+ )
+ deleteAlbumArt = false
+ dataChanged()
+ setResult(Activity.RESULT_OK)
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ showToast(R.string.error_load_failed, Toast.LENGTH_LONG)
+ }
+
+ override fun setResource(resource: BitmapPaletteWrapper?) {}
+ })
+ }
+
+ companion object {
+ val TAG: String = SongTagEditorActivity::class.java.simpleName
+ }
+
+ override val editorImage: ImageView
+ get() = binding.editorImage
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/TagWriter.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/TagWriter.kt
new file mode 100644
index 000000000..b9d5f28dd
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/TagWriter.kt
@@ -0,0 +1,204 @@
+package code.name.monkey.retromusic.activities.tageditor
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Bitmap
+import android.media.MediaScannerConnection
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.showToast
+import code.name.monkey.retromusic.misc.UpdateToastMediaScannerCompletionListener
+import code.name.monkey.retromusic.model.AudioTagInfo
+import code.name.monkey.retromusic.util.MusicUtil.createAlbumArtFile
+import code.name.monkey.retromusic.util.MusicUtil.deleteAlbumArt
+import code.name.monkey.retromusic.util.MusicUtil.insertAlbumArt
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jaudiotagger.audio.AudioFileIO
+import org.jaudiotagger.audio.exceptions.CannotReadException
+import org.jaudiotagger.audio.exceptions.CannotWriteException
+import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException
+import org.jaudiotagger.audio.exceptions.ReadOnlyFileException
+import org.jaudiotagger.tag.FieldDataInvalidException
+import org.jaudiotagger.tag.TagException
+import org.jaudiotagger.tag.images.AndroidArtwork
+import org.jaudiotagger.tag.images.Artwork
+import java.io.File
+import java.io.IOException
+
+class TagWriter {
+
+ companion object {
+
+ suspend fun scan(context: Context, toBeScanned: List?) {
+ if (toBeScanned.isNullOrEmpty()) {
+ Log.i("scan", "scan: Empty")
+ context.showToast("Scan file from folder")
+ return
+ }
+ MediaScannerConnection.scanFile(
+ context,
+ toBeScanned.toTypedArray(),
+ null,
+ withContext(Dispatchers.Main) {
+ if (context is Activity) UpdateToastMediaScannerCompletionListener(
+ context, toBeScanned
+ ) else null
+ }
+ )
+ }
+
+ suspend fun writeTagsToFiles(context: Context, info: AudioTagInfo) {
+ withContext(Dispatchers.IO) {
+ var artwork: Artwork? = null
+ var albumArtFile: File? = null
+ if (info.artworkInfo?.artwork != null) {
+ try {
+ albumArtFile = createAlbumArtFile(context).canonicalFile
+ info.artworkInfo.artwork.compress(
+ Bitmap.CompressFormat.JPEG,
+ 100,
+ albumArtFile.outputStream()
+ )
+ artwork = AndroidArtwork.createArtworkFromFile(albumArtFile)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ var wroteArtwork = false
+ var deletedArtwork = false
+ for (filePath in info.filePaths!!) {
+ try {
+ val audioFile = AudioFileIO.read(File(filePath))
+ val tag = audioFile.tagOrCreateAndSetDefault
+ if (info.fieldKeyValueMap != null) {
+ for ((key, value) in info.fieldKeyValueMap) {
+ try {
+ tag.setField(key, value)
+ } catch (e: FieldDataInvalidException) {
+ withContext(Dispatchers.Main) {
+ context.showToast(R.string.could_not_write_tags_to_file)
+ }
+ return@withContext listOf()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+ if (info.artworkInfo != null) {
+ if (info.artworkInfo.artwork == null) {
+ tag.deleteArtworkField()
+ deletedArtwork = true
+ } else if (artwork != null) {
+ tag.deleteArtworkField()
+ tag.setField(artwork)
+ wroteArtwork = true
+ }
+ }
+ audioFile.commit()
+ } catch (e: CannotReadException) {
+ e.printStackTrace()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } catch (e: CannotWriteException) {
+ e.printStackTrace()
+ } catch (e: TagException) {
+ e.printStackTrace()
+ } catch (e: ReadOnlyFileException) {
+ e.printStackTrace()
+ } catch (e: InvalidAudioFrameException) {
+ e.printStackTrace()
+ }
+ }
+ if (wroteArtwork) {
+ insertAlbumArt(context, info.artworkInfo!!.albumId, albumArtFile!!.path)
+ } else if (deletedArtwork) {
+ deleteAlbumArt(context, info.artworkInfo!!.albumId)
+ }
+ scan(context, info.filePaths)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ suspend fun writeTagsToFilesR(context: Context, info: AudioTagInfo): List =
+ withContext(Dispatchers.IO) {
+ val cacheFiles = mutableListOf()
+ var artwork: Artwork? = null
+ var albumArtFile: File? = null
+ if (info.artworkInfo?.artwork != null) {
+ try {
+ albumArtFile = createAlbumArtFile(context).canonicalFile
+ info.artworkInfo.artwork.compress(
+ Bitmap.CompressFormat.JPEG,
+ 100,
+ albumArtFile.outputStream()
+ )
+ artwork = AndroidArtwork.createArtworkFromFile(albumArtFile)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ var wroteArtwork = false
+ var deletedArtwork = false
+ for (filePath in info.filePaths!!) {
+ try {
+ val originFile = File(filePath)
+ val cacheFile = File(context.cacheDir, originFile.name)
+ cacheFiles.add(cacheFile)
+ originFile.inputStream().use { input ->
+ cacheFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+ val audioFile = AudioFileIO.read(cacheFile)
+ val tag = audioFile.tagOrCreateAndSetDefault
+ if (info.fieldKeyValueMap != null) {
+ for ((key, value) in info.fieldKeyValueMap) {
+ try {
+ tag.setField(key, value)
+ } catch (e: FieldDataInvalidException) {
+ withContext(Dispatchers.Main) {
+ context.showToast(R.string.could_not_write_tags_to_file)
+ }
+ return@withContext listOf()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+ if (info.artworkInfo != null) {
+ if (info.artworkInfo.artwork == null) {
+ tag.deleteArtworkField()
+ deletedArtwork = true
+ } else if (artwork != null) {
+ tag.deleteArtworkField()
+ tag.setField(artwork)
+ wroteArtwork = true
+ }
+ }
+ audioFile.commit()
+ } catch (e: CannotReadException) {
+ e.printStackTrace()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ } catch (e: CannotWriteException) {
+ e.printStackTrace()
+ } catch (e: TagException) {
+ e.printStackTrace()
+ } catch (e: ReadOnlyFileException) {
+ e.printStackTrace()
+ } catch (e: InvalidAudioFrameException) {
+ e.printStackTrace()
+ }
+ }
+ if (wroteArtwork) {
+ insertAlbumArt(context, info.artworkInfo!!.albumId, albumArtFile!!.path)
+ } else if (deletedArtwork) {
+ deleteAlbumArt(context, info.artworkInfo!!.albumId)
+ }
+ cacheFiles
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.kt
new file mode 100644
index 000000000..dd5cda1d3
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/CategoryInfoAdapter.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+package code.name.monkey.retromusic.adapter
+
+import android.annotation.SuppressLint
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.appthemehelper.ThemeStore.Companion.accentColor
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.PreferenceDialogLibraryCategoriesListitemBinding
+import code.name.monkey.retromusic.extensions.showToast
+import code.name.monkey.retromusic.model.CategoryInfo
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.SwipeAndDragHelper
+import code.name.monkey.retromusic.util.SwipeAndDragHelper.ActionCompletionContract
+
+class CategoryInfoAdapter : RecyclerView.Adapter(),
+ ActionCompletionContract {
+ var categoryInfos: MutableList =
+ PreferenceUtil.libraryCategory.toMutableList()
+ @SuppressLint("NotifyDataSetChanged")
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+ private val touchHelper: ItemTouchHelper
+ fun attachToRecyclerView(recyclerView: RecyclerView?) {
+ touchHelper.attachToRecyclerView(recyclerView)
+ }
+
+ override fun getItemCount(): Int {
+ return categoryInfos.size
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val categoryInfo = categoryInfos[position]
+ holder.binding.checkbox.isChecked = categoryInfo.visible
+ holder.binding.title.text =
+ holder.binding.title.resources.getString(categoryInfo.category.stringRes)
+ holder.itemView.setOnClickListener {
+ if (!(categoryInfo.visible && isLastCheckedCategory(categoryInfo))) {
+ categoryInfo.visible = !categoryInfo.visible
+ holder.binding.checkbox.isChecked = categoryInfo.visible
+ } else {
+ holder.itemView.context.showToast(R.string.you_have_to_select_at_least_one_category)
+ }
+ }
+ holder.binding.dragView.setOnTouchListener { _: View?, event: MotionEvent ->
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+ touchHelper.startDrag(holder)
+ }
+ false
+ }
+ }
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup, viewType: Int
+ ): ViewHolder {
+ return ViewHolder(
+ PreferenceDialogLibraryCategoriesListitemBinding.inflate(
+ LayoutInflater.from(
+ parent.context
+ ), parent, false
+ )
+ )
+ }
+
+ override fun onViewMoved(oldPosition: Int, newPosition: Int) {
+ val categoryInfo = categoryInfos[oldPosition]
+ categoryInfos.removeAt(oldPosition)
+ categoryInfos.add(newPosition, categoryInfo)
+ notifyItemMoved(oldPosition, newPosition)
+ }
+
+ private fun isLastCheckedCategory(categoryInfo: CategoryInfo): Boolean {
+ if (categoryInfo.visible) {
+ for (c in categoryInfos) {
+ if (c !== categoryInfo && c.visible) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+
+ class ViewHolder(val binding: PreferenceDialogLibraryCategoriesListitemBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ init {
+ binding.checkbox.buttonTintList =
+ ColorStateList.valueOf(accentColor(binding.checkbox.context))
+ }
+ }
+
+ init {
+ val swipeAndDragHelper = SwipeAndDragHelper(this)
+ touchHelper = ItemTouchHelper(swipeAndDragHelper)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..b139a0af8
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/GenreAdapter.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.ItemGenreBinding
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.interfaces.IGenreClickListener
+import code.name.monkey.retromusic.model.Genre
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+import java.util.*
+
+/**
+ * @author Hemanth S (h4h13).
+ */
+
+class GenreAdapter(
+ private val activity: FragmentActivity,
+ var dataSet: List,
+ private val listener: IGenreClickListener
+) : RecyclerView.Adapter() {
+
+ init {
+ this.setHasStableIds(true)
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].id
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(ItemGenreBinding.inflate(LayoutInflater.from(activity), parent, false))
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val genre = dataSet[position]
+ holder.binding.title.text = genre.name
+ holder.binding.text.text = String.format(
+ Locale.getDefault(),
+ "%d %s",
+ genre.songCount,
+ if (genre.songCount > 1) activity.getString(R.string.songs) else activity.getString(R.string.song)
+ )
+ loadGenreImage(genre, holder)
+ }
+
+ private fun loadGenreImage(genre: Genre, holder: GenreAdapter.ViewHolder) {
+ val genreSong = MusicUtil.songByGenre(genre.id)
+ Glide.with(activity)
+ .asBitmapPalette()
+ .songCoverOptions(genreSong)
+ .load(RetroGlideExtension.getSongModel(genreSong))
+ .into(object : RetroMusicColoredTarget(holder.binding.image) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(holder, colors)
+ }
+ })
+ // Just for a bit of shadow around image
+ holder.binding.image.outlineProvider = ViewOutlineProvider.BOUNDS
+ }
+
+ private fun setColors(holder: ViewHolder, color: MediaNotificationProcessor) {
+ holder.binding.imageContainerCard.setCardBackgroundColor(color.backgroundColor)
+ holder.binding.title.setTextColor(color.primaryTextColor)
+ holder.binding.text.setTextColor(color.secondaryTextColor)
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun swapDataSet(list: List) {
+ dataSet = list
+ notifyDataSetChanged()
+ }
+
+ inner class ViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root),
+ View.OnClickListener {
+ override fun onClick(v: View?) {
+ listener.onClickGenre(dataSet[layoutPosition], itemView)
+ }
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+ }
+}
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
new file mode 100644
index 000000000..e3f809b5f
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.os.bundleOf
+import androidx.fragment.app.findFragment
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.FragmentNavigatorExtras
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.retromusic.*
+import code.name.monkey.retromusic.adapter.album.AlbumAdapter
+import code.name.monkey.retromusic.adapter.artist.ArtistAdapter
+import code.name.monkey.retromusic.adapter.song.SongAdapter
+import code.name.monkey.retromusic.fragments.home.HomeFragment
+import code.name.monkey.retromusic.interfaces.IAlbumClickListener
+import code.name.monkey.retromusic.interfaces.IArtistClickListener
+import code.name.monkey.retromusic.model.Album
+import code.name.monkey.retromusic.model.Artist
+import code.name.monkey.retromusic.model.Home
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.PreferenceUtil
+
+class HomeAdapter(private val activity: AppCompatActivity) :
+ RecyclerView.Adapter(), IArtistClickListener, IAlbumClickListener {
+
+ private var list = listOf()
+
+ override fun getItemViewType(position: Int): Int {
+ return list[position].homeSection
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val layout =
+ LayoutInflater.from(activity).inflate(R.layout.section_recycler_view, parent, false)
+ return when (viewType) {
+ RECENT_ARTISTS, TOP_ARTISTS -> ArtistViewHolder(layout)
+ FAVOURITES -> PlaylistViewHolder(layout)
+ TOP_ALBUMS, RECENT_ALBUMS -> AlbumViewHolder(layout)
+ else -> {
+ ArtistViewHolder(layout)
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val home = list[position]
+ when (getItemViewType(position)) {
+ RECENT_ALBUMS -> {
+ val viewHolder = holder as AlbumViewHolder
+ viewHolder.bindView(home)
+ viewHolder.clickableArea.setOnClickListener {
+ it.findFragment().setSharedAxisXTransitions()
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.detailListFragment,
+ bundleOf("type" to RECENT_ALBUMS)
+ )
+ }
+ }
+ TOP_ALBUMS -> {
+ val viewHolder = holder as AlbumViewHolder
+ viewHolder.bindView(home)
+ viewHolder.clickableArea.setOnClickListener {
+ it.findFragment().setSharedAxisXTransitions()
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.detailListFragment,
+ bundleOf("type" to TOP_ALBUMS)
+ )
+ }
+ }
+ RECENT_ARTISTS -> {
+ val viewHolder = holder as ArtistViewHolder
+ viewHolder.bindView(home)
+ viewHolder.clickableArea.setOnClickListener {
+ it.findFragment().setSharedAxisXTransitions()
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.detailListFragment,
+ bundleOf("type" to RECENT_ARTISTS)
+ )
+ }
+ }
+ TOP_ARTISTS -> {
+ val viewHolder = holder as ArtistViewHolder
+ viewHolder.bindView(home)
+ viewHolder.clickableArea.setOnClickListener {
+ it.findFragment().setSharedAxisXTransitions()
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.detailListFragment,
+ bundleOf("type" to TOP_ARTISTS)
+ )
+ }
+ }
+ FAVOURITES -> {
+ val viewHolder = holder as PlaylistViewHolder
+ viewHolder.bindView(home)
+ viewHolder.clickableArea.setOnClickListener {
+ it.findFragment().setSharedAxisXTransitions()
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.detailListFragment,
+ bundleOf("type" to FAVOURITES)
+ )
+ }
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return list.size
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun swapData(sections: List) {
+ list = sections
+ notifyDataSetChanged()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private inner class AlbumViewHolder(view: View) : AbsHomeViewItem(view) {
+ fun bindView(home: Home) {
+ title.setText(home.titleRes)
+ recyclerView.apply {
+ adapter = albumAdapter(home.arrayList as List)
+ layoutManager = gridLayoutManager()
+ }
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private inner class ArtistViewHolder(view: View) : AbsHomeViewItem(view) {
+ fun bindView(home: Home) {
+ title.setText(home.titleRes)
+ recyclerView.apply {
+ layoutManager = linearLayoutManager()
+ adapter = artistsAdapter(home.arrayList as List)
+ }
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private inner class PlaylistViewHolder(view: View) : AbsHomeViewItem(view) {
+ fun bindView(home: Home) {
+ title.setText(home.titleRes)
+ recyclerView.apply {
+ val songAdapter = SongAdapter(
+ activity,
+ home.arrayList as MutableList,
+ R.layout.item_favourite_card
+ )
+ layoutManager = linearLayoutManager()
+ adapter = songAdapter
+ }
+ }
+ }
+
+ open class AbsHomeViewItem(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val recyclerView: RecyclerView = itemView.findViewById(R.id.recyclerView)
+ val title: AppCompatTextView = itemView.findViewById(R.id.title)
+ val clickableArea: ViewGroup = itemView.findViewById(R.id.clickable_area)
+ }
+
+ private fun artistsAdapter(artists: List) =
+ ArtistAdapter(activity, artists, PreferenceUtil.homeArtistGridStyle, this)
+
+ private fun albumAdapter(albums: List) =
+ AlbumAdapter(activity, albums, PreferenceUtil.homeAlbumGridStyle, this)
+
+ private fun gridLayoutManager() =
+ GridLayoutManager(activity, 1, GridLayoutManager.HORIZONTAL, false)
+
+ private fun linearLayoutManager() =
+ LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
+
+ override fun onArtist(artistId: Long, view: View) {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.artistDetailsFragment,
+ bundleOf(EXTRA_ARTIST_ID to artistId),
+ null,
+ FragmentNavigatorExtras(
+ view to artistId.toString()
+ )
+ )
+ }
+
+ override fun onAlbumClick(albumId: Long, view: View) {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.albumDetailsFragment,
+ bundleOf(EXTRA_ALBUM_ID to albumId),
+ null,
+ FragmentNavigatorExtras(
+ view to albumId.toString()
+ )
+ )
+ }
+}
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
new file mode 100644
index 000000000..22a60bd95
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.core.view.isGone
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.appthemehelper.ThemeStore
+import code.name.monkey.retromusic.*
+import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder
+import code.name.monkey.retromusic.db.PlaylistWithSongs
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.albumCoverOptions
+import code.name.monkey.retromusic.glide.RetroGlideExtension.artistImageOptions
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.helper.menu.SongMenuHelper
+import code.name.monkey.retromusic.model.Album
+import code.name.monkey.retromusic.model.Artist
+import code.name.monkey.retromusic.model.Genre
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.MusicUtil
+import com.bumptech.glide.Glide
+import java.util.*
+
+class SearchAdapter(
+ private val activity: FragmentActivity,
+ private var dataSet: List
+) : RecyclerView.Adapter() {
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun swapDataSet(dataSet: List) {
+ this.dataSet = dataSet
+ notifyDataSetChanged()
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ if (dataSet[position] is Album) return ALBUM
+ if (dataSet[position] is Artist) return if ((dataSet[position] as Artist).isAlbumArtist) ALBUM_ARTIST else ARTIST
+ if (dataSet[position] is Genre) return GENRE
+ if (dataSet[position] is PlaylistWithSongs) return PLAYLIST
+ return if (dataSet[position] is Song) SONG else HEADER
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return when (viewType) {
+ HEADER -> ViewHolder(
+ LayoutInflater.from(activity).inflate(
+ R.layout.sub_header,
+ parent,
+ false
+ ), viewType
+ )
+
+ ALBUM, ARTIST, ALBUM_ARTIST -> ViewHolder(
+ LayoutInflater.from(activity).inflate(
+ R.layout.item_list_big,
+ parent,
+ false
+ ), viewType
+ )
+
+ else -> ViewHolder(
+ LayoutInflater.from(activity).inflate(R.layout.item_list, parent, false),
+ viewType
+ )
+ }
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ when (getItemViewType(position)) {
+ ALBUM -> {
+ holder.imageTextContainer?.isVisible = true
+ val album = dataSet[position] as Album
+ holder.title?.text = album.title
+ holder.text?.text = album.artistName
+ Glide.with(activity).asDrawable().albumCoverOptions(album.safeGetFirstSong())
+ .load(RetroGlideExtension.getSongModel(album.safeGetFirstSong()))
+ .into(holder.image!!)
+ }
+
+ ARTIST -> {
+ holder.imageTextContainer?.isVisible = true
+ val artist = dataSet[position] as Artist
+ holder.title?.text = artist.name
+ holder.text?.text = MusicUtil.getArtistInfoString(activity, artist)
+ Glide.with(activity).asDrawable().artistImageOptions(artist).load(
+ RetroGlideExtension.getArtistModel(artist)
+ ).into(holder.image!!)
+ }
+
+ SONG -> {
+ holder.imageTextContainer?.isVisible = true
+ val song = dataSet[position] as Song
+ holder.title?.text = song.title
+ holder.text?.text = song.albumName
+ Glide.with(activity).asDrawable().songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song)).into(holder.image!!)
+ }
+
+ GENRE -> {
+ val genre = dataSet[position] as Genre
+ holder.title?.text = genre.name
+ holder.text?.text = String.format(
+ Locale.getDefault(),
+ "%d %s",
+ genre.songCount,
+ if (genre.songCount > 1) activity.getString(R.string.songs) else activity.getString(
+ R.string.song
+ )
+ )
+ }
+
+ PLAYLIST -> {
+ val playlist = dataSet[position] as PlaylistWithSongs
+ holder.title?.text = playlist.playlistEntity.playlistName
+ //holder.text?.text = MusicUtil.playlistInfoString(activity, playlist.songs)
+ }
+
+ ALBUM_ARTIST -> {
+ holder.imageTextContainer?.isVisible = true
+ val artist = dataSet[position] as Artist
+ holder.title?.text = artist.name
+ holder.text?.text = MusicUtil.getArtistInfoString(activity, artist)
+ Glide.with(activity).asDrawable().artistImageOptions(artist).load(
+ RetroGlideExtension.getArtistModel(artist)
+ ).into(holder.image!!)
+ }
+
+ else -> {
+ holder.title?.text = dataSet[position].toString()
+ holder.title?.setTextColor(ThemeStore.accentColor(activity))
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ inner class ViewHolder(itemView: View, itemViewType: Int) : MediaEntryViewHolder(itemView) {
+ init {
+ itemView.setOnLongClickListener(null)
+ imageTextContainer?.isInvisible = true
+ if (itemViewType == SONG) {
+ imageTextContainer?.isGone = true
+ menu?.isVisible = true
+ menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) {
+ override val song: Song
+ get() = dataSet[layoutPosition] as Song
+ })
+ } else {
+ menu?.isVisible = false
+ }
+
+ when (itemViewType) {
+ ALBUM -> setImageTransitionName(activity.getString(R.string.transition_album_art))
+ ARTIST -> setImageTransitionName(activity.getString(R.string.transition_artist_image))
+ else -> {
+ val container = itemView.findViewById(R.id.imageContainer)
+ container?.isVisible = false
+ }
+ }
+ }
+
+ override fun onClick(v: View?) {
+ val item = dataSet[layoutPosition]
+ when (itemViewType) {
+ ALBUM -> {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.albumDetailsFragment,
+ bundleOf(EXTRA_ALBUM_ID to (item as Album).id)
+ )
+ }
+
+ ARTIST -> {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.artistDetailsFragment,
+ bundleOf(EXTRA_ARTIST_ID to (item as Artist).id)
+ )
+ }
+
+ ALBUM_ARTIST -> {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.albumArtistDetailsFragment,
+ bundleOf(EXTRA_ARTIST_NAME to (item as Artist).name)
+ )
+ }
+
+ GENRE -> {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.genreDetailsFragment,
+ bundleOf(EXTRA_GENRE to (item as Genre))
+ )
+ }
+
+ PLAYLIST -> {
+ activity.findNavController(R.id.fragment_container).navigate(
+ R.id.playlistDetailsFragment,
+ bundleOf(EXTRA_PLAYLIST_ID to (item as PlaylistWithSongs).playlistEntity.playListId)
+ )
+ }
+
+ SONG -> {
+ MusicPlayerRemote.playNext(item as Song)
+ MusicPlayerRemote.playNextSong()
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val HEADER = 0
+ private const val ALBUM = 1
+ private const val ARTIST = 2
+ private const val SONG = 3
+ private const val GENRE = 4
+ private const val PLAYLIST = 5
+ private const val ALBUM_ARTIST = 6
+ }
+}
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
new file mode 100644
index 000000000..4005c6e6c
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SongFileAdapter.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.graphics.PorterDuff
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import code.name.monkey.appthemehelper.util.ATHUtil
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.audiocover.AudioFileCover
+import code.name.monkey.retromusic.interfaces.ICallbacks
+import code.name.monkey.retromusic.util.MusicUtil
+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
+
+class SongFileAdapter(
+ override val activity: AppCompatActivity,
+ private var dataSet: List,
+ private val itemLayoutRes: Int,
+ private val iCallbacks: ICallbacks?
+) : AbsMultiSelectAdapter(
+ activity, R.menu.menu_media_selection
+), PopupTextProvider {
+
+ init {
+ this.setHasStableIds(true)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (dataSet[position].isDirectory) FOLDER else FILE
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].hashCode().toLong()
+ }
+
+ fun swapDataSet(songFiles: List) {
+ this.dataSet = songFiles
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false))
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, index: Int) {
+ val file = dataSet[index]
+ holder.itemView.isActivated = isChecked(file)
+ holder.title?.text = getFileTitle(file)
+ if (holder.text != null) {
+ if (holder.itemViewType == FILE) {
+ holder.text?.text = getFileText(file)
+ } else {
+ holder.text?.isVisible = false
+ }
+ }
+
+ if (holder.image != null) {
+ loadFileImage(file, holder)
+ }
+ }
+
+ private fun getFileTitle(file: File): String {
+ return file.name
+ }
+
+ private fun getFileText(file: File): String? {
+ return if (file.isDirectory) null else readableFileSize(file.length())
+ }
+
+ private fun loadFileImage(file: File, holder: ViewHolder) {
+ val iconColor = ATHUtil.resolveColor(activity, androidx.appcompat.R.attr.colorControlNormal)
+ if (file.isDirectory) {
+ holder.image?.let {
+ it.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN)
+ it.setImageResource(R.drawable.ic_folder)
+ }
+ holder.imageTextContainer?.setCardBackgroundColor(
+ ATHUtil.resolveColor(
+ activity,
+ com.google.android.material.R.attr.colorSurface
+ )
+ )
+ } else {
+ val error = activity.getTintedDrawable(R.drawable.ic_audio_file, iconColor)
+ Glide.with(activity)
+ .load(AudioFileCover(file.path))
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .error(error)
+ .placeholder(error)
+ .transition(RetroGlideExtension.getDefaultTransition())
+ .signature(MediaStoreSignature("", file.lastModified(), 0))
+ .into(holder.image!!)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ override fun getIdentifier(position: Int): File {
+ return dataSet[position]
+ }
+
+ override fun getName(model: File): String {
+ return getFileTitle(model)
+ }
+
+ override fun onMultipleItemAction(menuItem: MenuItem, selection: List) {
+ if (iCallbacks == null) return
+ iCallbacks.onMultipleItemAction(menuItem, selection as ArrayList)
+ }
+
+ override fun getPopupText(position: Int): String {
+ return if (position >= dataSet.lastIndex) "" else getSectionName(position)
+ }
+
+ private fun getSectionName(position: Int): String {
+ return MusicUtil.getSectionName(dataSet[position].name)
+ }
+
+ inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
+
+ init {
+ if (menu != null && iCallbacks != null) {
+ menu?.setOnClickListener { v ->
+ val position = layoutPosition
+ if (isPositionInRange(position)) {
+ iCallbacks.onFileMenuClicked(dataSet[position], v)
+ }
+ }
+ }
+ if (imageTextContainer != null) {
+ imageTextContainer?.cardElevation = 0f
+ }
+ }
+
+ override fun onClick(v: View?) {
+ val position = layoutPosition
+ if (isPositionInRange(position)) {
+ if (isInQuickSelectMode) {
+ toggleChecked(position)
+ } else {
+ iCallbacks?.onFileSelected(dataSet[position])
+ }
+ }
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ val position = layoutPosition
+ return isPositionInRange(position) && toggleChecked(position)
+ }
+
+ private fun isPositionInRange(position: Int): Boolean {
+ return position >= 0 && position < dataSet.size
+ }
+ }
+
+ companion object {
+
+ private const val FILE = 0
+ private const val FOLDER = 1
+
+ fun readableFileSize(size: Long): String {
+ if (size <= 0) return "$size B"
+ val units = arrayOf("B", "KB", "MB", "GB", "TB")
+ val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()
+ return DecimalFormat("#,##0.##").format(size / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
+ }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/StorageAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/StorageAdapter.kt
new file mode 100644
index 000000000..98c2c632f
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/StorageAdapter.kt
@@ -0,0 +1,55 @@
+package code.name.monkey.retromusic.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.retromusic.R
+import java.io.File
+
+class StorageAdapter(
+ val storageList: List,
+ val storageClickListener: StorageClickListener
+) :
+ RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.item_storage,
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bindData(storageList[position])
+ }
+
+ override fun getItemCount(): Int {
+ return storageList.size
+ }
+
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val title: TextView = itemView.findViewById(R.id.title)
+
+ fun bindData(storage: Storage) {
+ title.text = storage.title
+ }
+
+ init {
+ itemView.setOnClickListener { storageClickListener.onStorageClicked(storageList[bindingAdapterPosition]) }
+ }
+ }
+}
+
+interface StorageClickListener {
+ fun onStorageClicked(storage: Storage)
+}
+
+class Storage {
+ lateinit var title: String
+ lateinit var file: File
+}
\ 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
new file mode 100644
index 000000000..160af7e47
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumAdapter.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
+import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.albumCoverOptions
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.helper.SortOrder
+import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
+import code.name.monkey.retromusic.interfaces.IAlbumClickListener
+import code.name.monkey.retromusic.model.Album
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+import me.zhanghai.android.fastscroll.PopupTextProvider
+
+open class AlbumAdapter(
+ override val activity: FragmentActivity,
+ var dataSet: List,
+ var itemLayoutRes: Int,
+ val listener: IAlbumClickListener?
+) : AbsMultiSelectAdapter(
+ activity,
+ R.menu.menu_media_selection
+), PopupTextProvider {
+
+ init {
+ this.setHasStableIds(true)
+ }
+
+ fun swapDataSet(dataSet: List) {
+ this.dataSet = dataSet
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
+ return createViewHolder(view, viewType)
+ }
+
+ protected open fun createViewHolder(view: View, viewType: Int): ViewHolder {
+ return ViewHolder(view)
+ }
+
+ private fun getAlbumTitle(album: Album): String {
+ return album.title
+ }
+
+ protected open fun getAlbumText(album: Album): String? {
+ return album.albumArtist.let {
+ if (it.isNullOrEmpty()) {
+ album.artistName
+ } else {
+ it
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val album = dataSet[position]
+ val isChecked = isChecked(album)
+ holder.itemView.isActivated = isChecked
+ holder.title?.text = getAlbumTitle(album)
+ holder.text?.text = getAlbumText(album)
+ // Check if imageContainer exists so we can have a smooth transition without
+ // CardView clipping, if it doesn't exist in current layout set transition name to image instead.
+ if (holder.imageContainer != null) {
+ holder.imageContainer?.transitionName = album.id.toString()
+ } else {
+ holder.image?.transitionName = album.id.toString()
+ }
+ loadAlbumCover(album, holder)
+ }
+
+ protected open fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) {
+ if (holder.paletteColorContainer != null) {
+ holder.title?.setTextColor(color.primaryTextColor)
+ holder.text?.setTextColor(color.secondaryTextColor)
+ holder.paletteColorContainer?.setBackgroundColor(color.backgroundColor)
+ }
+ holder.mask?.backgroundTintList = ColorStateList.valueOf(color.primaryTextColor)
+ holder.imageContainerCard?.setCardBackgroundColor(color.backgroundColor)
+ }
+
+ protected open fun loadAlbumCover(album: Album, holder: ViewHolder) {
+ if (holder.image == null) {
+ return
+ }
+ val song = album.safeGetFirstSong()
+ Glide.with(activity)
+ .asBitmapPalette()
+ .albumCoverOptions(song)
+ //.checkIgnoreMediaStore()
+ .load(RetroGlideExtension.getSongModel(song))
+ .into(object : RetroMusicColoredTarget(holder.image!!) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(colors, holder)
+ }
+ })
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].id
+ }
+
+ override fun getIdentifier(position: Int): Album? {
+ return dataSet[position]
+ }
+
+ override fun getName(model: Album): String {
+ return model.title
+ }
+
+ override fun onMultipleItemAction(
+ menuItem: MenuItem,
+ selection: List
+ ) {
+ SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId)
+ }
+
+ private fun getSongList(albums: List): List {
+ val songs = ArrayList()
+ for (album in albums) {
+ songs.addAll(album.songs)
+ }
+ return songs
+ }
+
+ override fun getPopupText(position: Int): String {
+ return getSectionName(position)
+ }
+
+ private fun getSectionName(position: Int): String {
+ var sectionName: String? = null
+ when (PreferenceUtil.albumSortOrder) {
+ SortOrder.AlbumSortOrder.ALBUM_A_Z, SortOrder.AlbumSortOrder.ALBUM_Z_A -> sectionName =
+ dataSet[position].title
+
+ SortOrder.AlbumSortOrder.ALBUM_ARTIST -> sectionName = dataSet[position].albumArtist
+ SortOrder.AlbumSortOrder.ALBUM_YEAR -> return MusicUtil.getYearString(
+ dataSet[position].year
+ )
+ }
+ return MusicUtil.getSectionName(sectionName)
+ }
+
+ inner class ViewHolder(itemView: View) : MediaEntryViewHolder(itemView) {
+
+ init {
+ menu?.isVisible = false
+ }
+
+ override fun onClick(v: View?) {
+ super.onClick(v)
+ if (isInQuickSelectMode) {
+ toggleChecked(layoutPosition)
+ } else {
+ image?.let {
+ listener?.onAlbumClick(dataSet[layoutPosition].id, imageContainer ?: it)
+ }
+ }
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ return toggleChecked(layoutPosition)
+ }
+ }
+
+ companion object {
+ val TAG: String = AlbumAdapter::class.java.simpleName
+ }
+}
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
new file mode 100644
index 000000000..5a0a7d3cd
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/AlbumCoverPagerAdapter.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.os.BundleCompat
+import androidx.core.os.bundleOf
+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.MainActivity
+import code.name.monkey.retromusic.fragments.AlbumCoverStyle
+import code.name.monkey.retromusic.fragments.NowPlayingScreen.*
+import code.name.monkey.retromusic.fragments.base.goToLyrics
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.misc.CustomFragmentStatePagerAdapter
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class AlbumCoverPagerAdapter(
+ fragmentManager: FragmentManager,
+ private val dataSet: List
+) : CustomFragmentStatePagerAdapter(fragmentManager) {
+
+ private var currentColorReceiver: AlbumCoverFragment.ColorReceiver? = null
+ private var currentColorReceiverPosition = -1
+
+ override fun getItem(position: Int): Fragment {
+ return AlbumCoverFragment.newInstance(dataSet[position])
+ }
+
+ override fun getCount(): Int {
+ return dataSet.size
+ }
+
+ override fun instantiateItem(container: ViewGroup, position: Int): Any {
+ val o = super.instantiateItem(container, position)
+ if (currentColorReceiver != null && currentColorReceiverPosition == position) {
+ receiveColor(currentColorReceiver!!, currentColorReceiverPosition)
+ }
+ return o
+ }
+
+ /**
+ * Only the latest passed [AlbumCoverFragment.ColorReceiver] is guaranteed to receive a
+ * response
+ */
+ fun receiveColor(colorReceiver: AlbumCoverFragment.ColorReceiver, position: Int) {
+
+ if (getFragment(position) is AlbumCoverFragment) {
+ val fragment = getFragment(position) as AlbumCoverFragment
+ currentColorReceiver = null
+ currentColorReceiverPosition = -1
+ fragment.receiveColor(colorReceiver, position)
+ } else {
+ currentColorReceiver = colorReceiver
+ currentColorReceiverPosition = position
+ }
+ }
+
+ class AlbumCoverFragment : Fragment() {
+
+ private var isColorReady: Boolean = false
+ private lateinit var color: MediaNotificationProcessor
+ private lateinit var song: Song
+ private var colorReceiver: ColorReceiver? = null
+ private var request: Int = 0
+ private val mainActivity get() = activity as MainActivity
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (arguments != null) {
+ song = BundleCompat.getParcelable(requireArguments(), SONG_ARG, Song::class.java)!!
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(getLayoutWithPlayerTheme(), container, false)
+ view.setOnClickListener {
+ if (mainActivity.getBottomSheetBehavior().state == STATE_EXPANDED) {
+ showLyricsDialog()
+ }
+ }
+ return view
+ }
+
+ private fun showLyricsDialog() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val data: String? = MusicUtil.getLyrics(song)
+ withContext(Dispatchers.Main) {
+ MaterialAlertDialogBuilder(
+ requireContext(),
+ com.google.android.material.R.style.ThemeOverlay_MaterialComponents_Dialog_Alert
+ ).apply {
+ setTitle(song.title)
+ setMessage(if (data.isNullOrEmpty()) "No lyrics found" else data)
+ setNegativeButton(R.string.synced_lyrics) { _, _ ->
+ goToLyrics(requireActivity())
+ }
+ show()
+ }
+ }
+ }
+ }
+
+ private fun getLayoutWithPlayerTheme(): Int {
+ return when (PreferenceUtil.nowPlayingScreen) {
+ Card, Fit, Tiny, Classic, Gradient, Full -> R.layout.fragment_album_full_cover
+ Peek -> R.layout.fragment_peek_album_cover
+ else -> {
+ if (PreferenceUtil.isCarouselEffect) {
+ R.layout.fragment_album_carousel_cover
+ } else {
+ when (PreferenceUtil.albumCoverStyle) {
+ AlbumCoverStyle.Normal -> R.layout.fragment_album_cover
+ AlbumCoverStyle.Flat -> R.layout.fragment_album_flat_cover
+ AlbumCoverStyle.Circle -> R.layout.fragment_album_circle_cover
+ AlbumCoverStyle.Card -> R.layout.fragment_album_card_cover
+ AlbumCoverStyle.Full -> R.layout.fragment_album_full_cover
+ AlbumCoverStyle.FullCard -> R.layout.fragment_album_full_card_cover
+ }
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ loadAlbumCover(albumCover = view.findViewById(R.id.player_image))
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ colorReceiver = null
+ }
+
+ private fun loadAlbumCover(albumCover: ImageView) {
+ Glide.with(this)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ //.checkIgnoreMediaStore()
+ .load(RetroGlideExtension.getSongModel(song))
+ .dontAnimate()
+ .into(object : RetroMusicColoredTarget(albumCover) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColor(colors)
+ }
+ })
+ }
+
+ private fun setColor(color: MediaNotificationProcessor) {
+ this.color = color
+ isColorReady = true
+ if (colorReceiver != null) {
+ colorReceiver!!.onColorReady(color, request)
+ colorReceiver = null
+ }
+ }
+
+ internal fun receiveColor(colorReceiver: ColorReceiver, request: Int) {
+ if (isColorReady) {
+ colorReceiver.onColorReady(color, request)
+ } else {
+ this.colorReceiver = colorReceiver
+ this.request = request
+ }
+ }
+
+ interface ColorReceiver {
+ fun onColorReady(color: MediaNotificationProcessor, request: Int)
+ }
+
+ companion object {
+
+ private const val SONG_ARG = "song"
+
+ fun newInstance(song: Song): AlbumCoverFragment {
+ val frag = AlbumCoverFragment()
+ frag.arguments = bundleOf(SONG_ARG to song)
+ return frag
+ }
+ }
+ }
+
+ companion object {
+ 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
new file mode 100644
index 000000000..7a9f7fa6e
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/album/HorizontalAlbumAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.albumCoverOptions
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.helper.HorizontalAdapterHelper
+import code.name.monkey.retromusic.interfaces.IAlbumClickListener
+import code.name.monkey.retromusic.model.Album
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+
+class HorizontalAlbumAdapter(
+ activity: FragmentActivity,
+ dataSet: List,
+ albumClickListener: IAlbumClickListener
+) : AlbumAdapter(
+ activity, dataSet, HorizontalAdapterHelper.LAYOUT_RES, albumClickListener
+) {
+
+ override fun createViewHolder(view: View, viewType: Int): ViewHolder {
+ val params = view.layoutParams as ViewGroup.MarginLayoutParams
+ HorizontalAdapterHelper.applyMarginToLayoutParams(activity, params, viewType)
+ return ViewHolder(view)
+ }
+
+ 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))
+ }
+
+ override fun loadAlbumCover(album: Album, holder: ViewHolder) {
+ if (holder.image == null) return
+ Glide.with(activity)
+ .asBitmapPalette()
+ .albumCoverOptions(album.safeGetFirstSong())
+ .load(RetroGlideExtension.getSongModel(album.safeGetFirstSong()))
+ .into(object : RetroMusicColoredTarget(holder.image!!) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(colors, holder)
+ }
+ })
+ }
+
+ override fun getAlbumText(album: Album): String {
+ return MusicUtil.getYearString(album.year)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return HorizontalAdapterHelper.getItemViewType(position, itemCount)
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ companion object {
+ val TAG: String = AlbumAdapter::class.java.simpleName
+ }
+}
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
new file mode 100644
index 000000000..e1ea2ca4c
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/artist/ArtistAdapter.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.annotation.SuppressLint
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
+import code.name.monkey.retromusic.extensions.hide
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.artistImageOptions
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
+import code.name.monkey.retromusic.interfaces.IAlbumArtistClickListener
+import code.name.monkey.retromusic.interfaces.IArtistClickListener
+import code.name.monkey.retromusic.model.Artist
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
+import com.bumptech.glide.Glide
+import me.zhanghai.android.fastscroll.PopupTextProvider
+
+class ArtistAdapter(
+ override val activity: FragmentActivity,
+ var dataSet: List,
+ var itemLayoutRes: Int,
+ val IArtistClickListener: IArtistClickListener,
+ val IAlbumArtistClickListener: IAlbumArtistClickListener? = null
+) : AbsMultiSelectAdapter(activity, R.menu.menu_media_selection),
+ PopupTextProvider {
+
+ var albumArtistsOnly = false
+
+ init {
+ this.setHasStableIds(true)
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun swapDataSet(dataSet: List) {
+ this.dataSet = dataSet
+ notifyDataSetChanged()
+ albumArtistsOnly = PreferenceUtil.albumArtistsOnly
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].id
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view =
+ try {
+ LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
+ } catch (e: Resources.NotFoundException) {
+ LayoutInflater.from(activity).inflate(R.layout.item_grid_circle, parent, false)
+ }
+ return createViewHolder(view)
+ }
+
+ private fun createViewHolder(view: View): ViewHolder {
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val artist = dataSet[position]
+ val isChecked = isChecked(artist)
+ holder.itemView.isActivated = isChecked
+ holder.title?.text = artist.name
+ holder.text?.hide()
+ val transitionName =
+ if (albumArtistsOnly) artist.name else artist.id.toString()
+ if (holder.imageContainer != null) {
+ holder.imageContainer?.transitionName = transitionName
+ } else {
+ holder.image?.transitionName = transitionName
+ }
+ loadArtistImage(artist, holder)
+ }
+
+ private fun setColors(processor: MediaNotificationProcessor, holder: ViewHolder) {
+ holder.mask?.backgroundTintList = ColorStateList.valueOf(processor.primaryTextColor)
+ if (holder.paletteColorContainer != null) {
+ holder.paletteColorContainer?.setBackgroundColor(processor.backgroundColor)
+ holder.title?.setTextColor(processor.primaryTextColor)
+ }
+ holder.imageContainerCard?.setCardBackgroundColor(processor.backgroundColor)
+ }
+
+ private fun loadArtistImage(artist: Artist, holder: ViewHolder) {
+ if (holder.image == null) {
+ return
+ }
+ Glide.with(activity)
+ .asBitmapPalette()
+ .artistImageOptions(artist)
+ .load(RetroGlideExtension.getArtistModel(artist))
+ .transition(RetroGlideExtension.getDefaultTransition())
+ .into(object : RetroMusicColoredTarget(holder.image!!) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(colors, holder)
+ }
+ })
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ override fun getIdentifier(position: Int): Artist {
+ return dataSet[position]
+ }
+
+ override fun getName(model: Artist): String {
+ return model.name
+ }
+
+ override fun onMultipleItemAction(
+ menuItem: MenuItem,
+ selection: List
+ ) {
+ SongsMenuHelper.handleMenuClick(activity, getSongList(selection), menuItem.itemId)
+ }
+
+ private fun getSongList(artists: List): List {
+ val songs = ArrayList()
+ for (artist in artists) {
+ songs.addAll(artist.songs) // maybe async in future?
+ }
+ return songs
+ }
+
+ override fun getPopupText(position: Int): String {
+ return getSectionName(position)
+ }
+
+ private fun getSectionName(position: Int): String {
+ return MusicUtil.getSectionName(dataSet[position].name)
+ }
+
+ inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
+
+ init {
+ menu?.isVisible = false
+ }
+
+ override fun onClick(v: View?) {
+ super.onClick(v)
+ if (isInQuickSelectMode) {
+ toggleChecked(layoutPosition)
+ } else {
+ val artist = dataSet[layoutPosition]
+ image?.let {
+ if (albumArtistsOnly && IAlbumArtistClickListener != null) {
+ IAlbumArtistClickListener.onAlbumArtist(artist.name, imageContainer ?: it)
+ } else {
+ IArtistClickListener.onArtist(artist.id, imageContainer ?: it)
+ }
+ }
+ }
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ return toggleChecked(layoutPosition)
+ }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/backup/BackupAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/backup/BackupAdapter.kt
new file mode 100644
index 000000000..adfcecf1b
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/backup/BackupAdapter.kt
@@ -0,0 +1,65 @@
+package code.name.monkey.retromusic.adapter.backup
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.ViewGroup
+import androidx.appcompat.widget.PopupMenu
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.ItemListBackupBinding
+import java.io.File
+
+
+class BackupAdapter(
+ val activity: FragmentActivity,
+ var dataSet: MutableList,
+ val backupClickedListener: BackupClickedListener
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ ItemListBackupBinding.inflate(LayoutInflater.from(activity), parent, false)
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.binding.title.text = dataSet[position].nameWithoutExtension
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun swapDataset(dataSet: List) {
+ this.dataSet = ArrayList(dataSet)
+ notifyDataSetChanged()
+ }
+
+ inner class ViewHolder(val binding: ItemListBackupBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ init {
+ binding.menu.setOnClickListener { view ->
+ val popupMenu = PopupMenu(activity, view)
+ popupMenu.inflate(R.menu.menu_backup)
+ popupMenu.setOnMenuItemClickListener { menuItem ->
+ return@setOnMenuItemClickListener backupClickedListener.onBackupMenuClicked(
+ dataSet[bindingAdapterPosition],
+ menuItem
+ )
+ }
+ popupMenu.show()
+ }
+ itemView.setOnClickListener {
+ backupClickedListener.onBackupClicked(dataSet[bindingAdapterPosition])
+ }
+ }
+ }
+
+ interface BackupClickedListener {
+ fun onBackupClicked(file: File)
+
+ fun onBackupMenuClicked(file: File, menuItem: MenuItem): Boolean
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.kt
new file mode 100644
index 000000000..bb7b8491c
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/AbsMultiSelectAdapter.kt
@@ -0,0 +1,131 @@
+package code.name.monkey.retromusic.adapter.base
+
+import android.graphics.Color
+import android.view.ActionMode
+import android.view.Menu
+import android.view.MenuItem
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.MenuRes
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.NumberRollViewBinding
+import code.name.monkey.retromusic.views.NumberRollView
+
+abstract class AbsMultiSelectAdapter(
+ open val activity: FragmentActivity, @MenuRes menuRes: Int,
+) : RecyclerView.Adapter(), ActionMode.Callback {
+ var actionMode: ActionMode? = null
+ private val checked: MutableList
+ private var menuRes: Int
+
+ override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ val inflater = mode?.menuInflater
+ inflater?.inflate(menuRes, menu)
+ return true
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+ return false
+ }
+
+ override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
+ if (item?.itemId == R.id.action_multi_select_adapter_check_all) {
+ checkAll()
+ } else {
+ onMultipleItemAction(item!!, ArrayList(checked))
+ actionMode?.finish()
+ clearChecked()
+ }
+ return true
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode?) {
+ clearChecked()
+ activity.window.statusBarColor = when {
+ VersionUtils.hasMarshmallow() -> Color.TRANSPARENT
+ else -> Color.BLACK
+ }
+ actionMode = null
+ onBackPressedCallback.remove()
+ }
+
+ private fun checkAll() {
+ if (actionMode != null) {
+ checked.clear()
+ for (i in 0 until itemCount) {
+ val identifier = getIdentifier(i)
+ if (identifier != null) {
+ checked.add(identifier)
+ }
+ }
+ notifyDataSetChanged()
+ updateCab()
+ }
+ }
+
+ protected abstract fun getIdentifier(position: Int): I?
+
+ protected abstract fun getName(model: I): String?
+
+ protected fun isChecked(identifier: I): Boolean {
+ return checked.contains(identifier)
+ }
+
+ protected val isInQuickSelectMode: Boolean
+ get() = actionMode != null
+
+ protected abstract fun onMultipleItemAction(menuItem: MenuItem, selection: List)
+ protected fun setMultiSelectMenuRes(@MenuRes menuRes: Int) {
+ this.menuRes = menuRes
+ }
+
+ protected fun toggleChecked(position: Int): Boolean {
+ val identifier = getIdentifier(position) ?: return false
+ if (!checked.remove(identifier)) {
+ checked.add(identifier)
+ }
+ notifyItemChanged(position)
+ updateCab()
+ return true
+ }
+
+ private fun clearChecked() {
+ checked.clear()
+ notifyDataSetChanged()
+ }
+
+ private fun updateCab() {
+ if (actionMode == null) {
+ actionMode = activity.startActionMode(this)?.apply {
+ customView = NumberRollViewBinding.inflate(activity.layoutInflater).root
+ }
+ activity.onBackPressedDispatcher.addCallback(onBackPressedCallback)
+ }
+ val size = checked.size
+ when {
+ size <= 0 -> {
+ actionMode?.finish()
+ }
+ else -> {
+ actionMode?.customView?.findViewById(R.id.selection_mode_number)
+ ?.setNumber(size, true)
+ }
+ }
+ }
+
+ init {
+ checked = ArrayList()
+ this.menuRes = menuRes
+ }
+
+ private val onBackPressedCallback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (actionMode != null) {
+ actionMode?.finish()
+ remove()
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..d68186c0f
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/base/MediaEntryViewHolder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+
+package code.name.monkey.retromusic.adapter.base;
+
+import android.graphics.Color;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+
+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 {
+
+ @Nullable
+ public View dragView;
+
+ @Nullable
+ public View dummyContainer;
+
+ @Nullable
+ public ImageView image;
+
+ @Nullable
+ public MaterialCardView imageContainerCard;
+
+ @Nullable
+ public FrameLayout imageContainer;
+
+ @Nullable
+ public TextView imageText;
+
+ @Nullable
+ public MaterialCardView imageTextContainer;
+
+ @Nullable
+ public View mask;
+
+ @Nullable
+ public AppCompatImageView menu;
+
+ @Nullable
+ public View paletteColorContainer;
+
+ @Nullable
+ public TextView text;
+
+ @Nullable
+ public TextView text2;
+
+ @Nullable
+ public TextView time;
+
+ @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);
+
+ image = itemView.findViewById(R.id.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);
+ imageContainer = itemView.findViewById(R.id.imageContainer);
+
+ menu = itemView.findViewById(R.id.menu);
+ dragView = itemView.findViewById(R.id.drag_view);
+ paletteColorContainer = itemView.findViewById(R.id.paletteColorContainer);
+ mask = itemView.findViewById(R.id.mask);
+ dummyContainer = itemView.findViewById(R.id.dummy_view);
+
+ 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);
+ }*/
+ }
+}
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
new file mode 100644
index 000000000..19d681ecf
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/LegacyPlaylistAdapter.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import code.name.monkey.retromusic.model.Playlist
+import code.name.monkey.retromusic.util.MusicUtil
+
+class LegacyPlaylistAdapter(
+ private val activity: FragmentActivity,
+ private var list: List,
+ private val layoutRes: Int,
+ private val playlistClickListener: PlaylistClickListener
+) :
+ RecyclerView.Adapter() {
+
+ fun swapData(list: List) {
+ this.list = list
+ notifyDataSetChanged()
+ }
+
+ class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView)
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val playlist: Playlist = list[position]
+ holder.title?.text = playlist.name
+ holder.text?.text = MusicUtil.getPlaylistInfoString(activity, playlist.getSongs())
+ holder.itemView.setOnClickListener {
+ playlistClickListener.onPlaylistClick(playlist)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return list.size
+ }
+
+ interface PlaylistClickListener {
+ fun onPlaylistClick(playlist: Playlist)
+ }
+}
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
new file mode 100755
index 000000000..6e5b0c001
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.Color
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.view.isGone
+import androidx.core.view.setPadding
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.appthemehelper.util.ATHUtil
+import code.name.monkey.appthemehelper.util.TintHelper
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
+import code.name.monkey.retromusic.db.PlaylistEntity
+import code.name.monkey.retromusic.db.PlaylistWithSongs
+import code.name.monkey.retromusic.db.toSongs
+import code.name.monkey.retromusic.extensions.dipToPix
+import code.name.monkey.retromusic.glide.RetroGlideExtension.playlistOptions
+import code.name.monkey.retromusic.glide.playlistPreview.PlaylistPreview
+import code.name.monkey.retromusic.helper.SortOrder.PlaylistSortOrder
+import code.name.monkey.retromusic.helper.menu.PlaylistMenuHelper
+import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
+import code.name.monkey.retromusic.interfaces.IPlaylistClickListener
+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.Glide
+import me.zhanghai.android.fastscroll.PopupTextProvider
+
+class PlaylistAdapter(
+ override val activity: FragmentActivity,
+ var dataSet: List,
+ private var itemLayoutRes: Int,
+ private val listener: IPlaylistClickListener
+) : AbsMultiSelectAdapter(
+ activity,
+ R.menu.menu_playlists_selection
+), PopupTextProvider {
+
+ init {
+ setHasStableIds(true)
+ }
+
+ fun swapDataSet(dataSet: List) {
+ this.dataSet = dataSet
+ notifyDataSetChanged()
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].playlistEntity.playListId
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
+ return createViewHolder(view)
+ }
+
+ private fun createViewHolder(view: View): ViewHolder {
+ return ViewHolder(view)
+ }
+
+ private fun getPlaylistTitle(playlist: PlaylistEntity): String {
+ return playlist.playlistName.ifEmpty { "-" }
+ }
+
+ private fun getPlaylistText(playlist: PlaylistWithSongs): String {
+ return MusicUtil.getPlaylistInfoString(activity, playlist.songs.toSongs())
+ }
+
+ override fun getPopupText(position: Int): String {
+ val sectionName: String = when (PreferenceUtil.playlistSortOrder) {
+ PlaylistSortOrder.PLAYLIST_A_Z, PlaylistSortOrder.PLAYLIST_Z_A -> dataSet[position].playlistEntity.playlistName
+ PlaylistSortOrder.PLAYLIST_SONG_COUNT, PlaylistSortOrder.PLAYLIST_SONG_COUNT_DESC -> dataSet[position].songs.size.toString()
+ else -> {
+ return ""
+ }
+ }
+ return MusicUtil.getSectionName(sectionName)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val playlist = dataSet[position]
+ holder.itemView.isActivated = isChecked(playlist)
+ holder.title?.text = getPlaylistTitle(playlist.playlistEntity)
+ holder.text?.text = getPlaylistText(playlist)
+ holder.menu?.isGone = isChecked(playlist)
+ if (itemLayoutRes == R.layout.item_list) {
+ holder.image?.setPadding(activity.dipToPix(8F).toInt())
+ holder.image?.setImageDrawable(getIconRes())
+ } else {
+ Glide.with(activity)
+ .load(PlaylistPreview(playlist))
+ .playlistOptions()
+ .into(holder.image!!)
+ }
+ }
+
+ private fun getIconRes(): Drawable = TintHelper.createTintedDrawable(
+ activity,
+ R.drawable.ic_playlist_play,
+ ATHUtil.resolveColor(activity, android.R.attr.colorControlNormal)
+ )
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ override fun getIdentifier(position: Int): PlaylistWithSongs {
+ return dataSet[position]
+ }
+
+ override fun getName(model: PlaylistWithSongs): String {
+ return model.playlistEntity.playlistName
+ }
+
+ override fun onMultipleItemAction(menuItem: MenuItem, selection: List) {
+ when (menuItem.itemId) {
+ else -> SongsMenuHelper.handleMenuClick(
+ activity,
+ getSongList(selection),
+ menuItem.itemId
+ )
+ }
+ }
+
+ private fun getSongList(playlists: List): List {
+ val songs = mutableListOf()
+ playlists.forEach {
+ songs.addAll(it.songs.toSongs())
+ }
+ return songs
+ }
+
+ inner class ViewHolder(itemView: View) : code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder(itemView) {
+ init {
+ menu?.setOnClickListener { view ->
+ val popupMenu = PopupMenu(activity, view)
+ popupMenu.inflate(R.menu.menu_item_playlist)
+ popupMenu.setOnMenuItemClickListener { item ->
+ PlaylistMenuHelper.handleMenuClick(activity, dataSet[layoutPosition], item)
+ }
+ popupMenu.show()
+ }
+
+ imageTextContainer?.apply {
+ cardElevation = 0f
+ setCardBackgroundColor(Color.TRANSPARENT)
+ }
+ }
+
+ override fun onClick(v: View?) {
+ if (isInQuickSelectMode) {
+ toggleChecked(layoutPosition)
+ } else {
+ itemView.transitionName = "playlist"
+ listener.onPlaylistClick(dataSet[layoutPosition], itemView)
+ }
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ toggleChecked(layoutPosition)
+ return true
+ }
+ }
+
+ companion object {
+ val TAG: String = PlaylistAdapter::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/ui/adapter/song/AbsOffsetSongAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt
similarity index 59%
rename from app/src/main/java/code/name/monkey/retromusic/ui/adapter/song/AbsOffsetSongAdapter.kt
rename to app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt
index 92e25f588..2d5fd724f 100644
--- a/app/src/main/java/code/name/monkey/retromusic/ui/adapter/song/AbsOffsetSongAdapter.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/AbsOffsetSongAdapter.kt
@@ -1,26 +1,38 @@
-package code.name.monkey.retromusic.ui.adapter.song
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
-import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentActivity
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.helper.MusicPlayerRemote
-import code.name.monkey.retromusic.interfaces.CabHolder
import code.name.monkey.retromusic.model.Song
-import java.util.*
-
-abstract class AbsOffsetSongAdapter : SongAdapter {
-
- constructor(activity: AppCompatActivity, dataSet: ArrayList, @LayoutRes itemLayoutRes: Int, usePalette: Boolean, cabHolder: CabHolder?) : super(activity, dataSet, itemLayoutRes, usePalette, cabHolder)
-
- constructor(activity: AppCompatActivity, dataSet: ArrayList, @LayoutRes itemLayoutRes: Int, usePalette: Boolean, cabHolder: CabHolder?, showSectionName: Boolean) : super(activity, dataSet, itemLayoutRes, usePalette, cabHolder, showSectionName) {}
+abstract class AbsOffsetSongAdapter(
+ activity: FragmentActivity,
+ dataSet: MutableList,
+ @LayoutRes itemLayoutRes: Int
+) : SongAdapter(activity, dataSet, itemLayoutRes) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongAdapter.ViewHolder {
if (viewType == OFFSET_ITEM) {
- val view = LayoutInflater.from(activity).inflate(R.layout.item_list_single_row, parent, false)
+ val view = LayoutInflater.from(activity)
+ .inflate(R.layout.item_list_quick_actions, parent, false)
return createViewHolder(view)
}
return super.onCreateViewHolder(parent, viewType)
@@ -51,29 +63,23 @@ abstract class AbsOffsetSongAdapter : SongAdapter {
return if (position == 0) OFFSET_ITEM else SONG
}
- override fun getSectionName(position: Int): String {
- var positionFinal = position
- positionFinal--
- return if (position < 0) "" else super.getSectionName(positionFinal)
- }
-
open inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) {
- override// could also return null, just to be safe return empty song
+ override // could also return null, just to be safe return empty song
val song: Song
- get() = if (itemViewType == OFFSET_ITEM) Song.emptySong else dataSet[adapterPosition - 1]
+ get() = if (itemViewType == OFFSET_ITEM) Song.emptySong else dataSet[layoutPosition - 1]
override fun onClick(v: View?) {
if (isInQuickSelectMode && itemViewType != OFFSET_ITEM) {
- toggleChecked(adapterPosition)
+ toggleChecked(layoutPosition)
} else {
- MusicPlayerRemote.openQueue(dataSet, adapterPosition - 1, true)
+ MusicPlayerRemote.openQueue(dataSet, layoutPosition - 1, true)
}
}
override fun onLongClick(v: View?): Boolean {
if (itemViewType == OFFSET_ITEM) return false
- toggleChecked(adapterPosition)
+ toggleChecked(layoutPosition)
return true
}
}
@@ -82,4 +88,4 @@ abstract class AbsOffsetSongAdapter : SongAdapter {
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
new file mode 100644
index 000000000..bf85fe28b
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/OrderablePlaylistSongAdapter.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.db.PlaylistEntity
+import code.name.monkey.retromusic.db.toSongEntity
+import code.name.monkey.retromusic.db.toSongsEntity
+import code.name.monkey.retromusic.dialogs.RemoveSongFromPlaylistDialog
+import code.name.monkey.retromusic.fragments.LibraryViewModel
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.ViewUtil
+import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter
+import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class OrderablePlaylistSongAdapter(
+ private val playlistId: Long,
+ activity: FragmentActivity,
+ dataSet: MutableList,
+ itemLayoutRes: Int,
+) : SongAdapter(activity, dataSet, itemLayoutRes),
+ DraggableItemAdapter {
+
+ val libraryViewModel: LibraryViewModel by activity.viewModel()
+
+ init {
+ this.setHasStableIds(true)
+ this.setMultiSelectMenuRes(R.menu.menu_playlists_songs_selection)
+ }
+
+ override fun getItemId(position: Int): Long {
+ // requires static value, it means need to keep the same value
+ // even if the item position has been changed.
+ return dataSet[position].id
+ }
+
+ override fun createViewHolder(view: View): SongAdapter.ViewHolder {
+ return ViewHolder(view)
+ }
+
+ override fun onMultipleItemAction(menuItem: MenuItem, selection: List) {
+ when (menuItem.itemId) {
+ R.id.action_remove_from_playlist -> RemoveSongFromPlaylistDialog.create(
+ selection.toSongsEntity(
+ playlistId
+ )
+ )
+ .show(activity.supportFragmentManager, "REMOVE_FROM_PLAYLIST")
+
+ else -> super.onMultipleItemAction(menuItem, selection)
+ }
+ }
+
+ inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) {
+
+ override var songMenuRes: Int
+ get() = R.menu.menu_item_playlist_song
+ set(value) {
+ super.songMenuRes = value
+ }
+
+ override fun onSongMenuItemClick(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_remove_from_playlist -> {
+ RemoveSongFromPlaylistDialog.create(song.toSongEntity(playlistId))
+ .show(activity.supportFragmentManager, "REMOVE_FROM_PLAYLIST")
+ return true
+ }
+ }
+ return super.onSongMenuItemClick(item)
+ }
+
+ init {
+ dragView?.isVisible = true
+ }
+ }
+
+ override fun onCheckCanStartDrag(holder: ViewHolder, position: Int, x: Int, y: Int): Boolean {
+ if (isInQuickSelectMode) {
+ return false
+ }
+ return ViewUtil.hitTest(holder.imageText!!, x, y) || ViewUtil.hitTest(
+ holder.dragView!!,
+ x,
+ y
+ )
+ }
+
+ override fun onMoveItem(fromPosition: Int, toPosition: Int) {
+ dataSet.add(toPosition, dataSet.removeAt(fromPosition))
+ }
+
+ override fun onGetItemDraggableRange(holder: ViewHolder, position: Int): ItemDraggableRange? {
+ return null
+ }
+
+ override fun onCheckCanDrop(draggingPosition: Int, dropPosition: Int): Boolean {
+ return true
+ }
+
+ override fun onItemDragStarted(position: Int) {
+ notifyDataSetChanged()
+ }
+
+ override fun onItemDragFinished(fromPosition: Int, toPosition: Int, result: Boolean) {
+ notifyDataSetChanged()
+ }
+
+ fun saveSongs(playlistEntity: PlaylistEntity) {
+ activity.lifecycleScope.launch(Dispatchers.IO) {
+ libraryViewModel.insertSongs(dataSet.toSongsEntity(playlistEntity))
+ }
+ }
+}
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
new file mode 100644
index 000000000..2d387be44
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/PlayingQueueAdapter.kt
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.helper.MusicPlayerRemote.isPlaying
+import code.name.monkey.retromusic.helper.MusicPlayerRemote.playNextSong
+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 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
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
+import me.zhanghai.android.fastscroll.PopupTextProvider
+
+class PlayingQueueAdapter(
+ activity: FragmentActivity,
+ dataSet: MutableList,
+ private var current: Int,
+ itemLayoutRes: Int,
+) : SongAdapter(activity, dataSet, itemLayoutRes),
+ DraggableItemAdapter,
+ SwipeableItemAdapter,
+ PopupTextProvider {
+
+ private var songToRemove: Song? = null
+
+ override fun createViewHolder(view: View): SongAdapter.ViewHolder {
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
+ super.onBindViewHolder(holder, position)
+ val song = dataSet[position]
+ holder.time?.text = MusicUtil.getReadableDurationString(song.duration)
+ if (holder.itemViewType == HISTORY || holder.itemViewType == CURRENT) {
+ setAlpha(holder, 0.5f)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ if (position < current) {
+ return HISTORY
+ } else if (position > current) {
+ return UP_NEXT
+ }
+ return CURRENT
+ }
+
+ override fun loadAlbumCover(song: Song, holder: SongAdapter.ViewHolder) {
+ if (holder.image == null) {
+ return
+ }
+ Glide.with(activity)
+ .load(RetroGlideExtension.getSongModel(song))
+ .songCoverOptions(song)
+ .into(holder.image!!)
+ }
+
+ fun swapDataSet(dataSet: List, position: Int) {
+ this.dataSet = dataSet.toMutableList()
+ current = position
+ notifyDataSetChanged()
+ }
+
+ fun setCurrent(current: Int) {
+ this.current = current
+ notifyDataSetChanged()
+ }
+
+ private fun setAlpha(holder: SongAdapter.ViewHolder, alpha: Float) {
+ holder.image?.alpha = alpha
+ holder.title?.alpha = alpha
+ holder.text?.alpha = alpha
+ holder.paletteColorContainer?.alpha = alpha
+ holder.dragView?.alpha = alpha
+ holder.menu?.alpha = alpha
+ }
+
+ override fun getPopupText(position: Int): String {
+ return MusicUtil.getSectionName(dataSet[position].title)
+ }
+
+ override fun onCheckCanStartDrag(holder: ViewHolder, position: Int, x: Int, y: Int): Boolean {
+ return ViewUtil.hitTest(holder.imageText!!, x, y) || ViewUtil.hitTest(
+ holder.dragView!!,
+ x,
+ y
+ )
+ }
+
+ override fun onGetItemDraggableRange(holder: ViewHolder, position: Int): ItemDraggableRange? {
+ return null
+ }
+
+ override fun onMoveItem(fromPosition: Int, toPosition: Int) {
+ MusicPlayerRemote.moveSong(fromPosition, toPosition)
+ }
+
+ override fun onCheckCanDrop(draggingPosition: Int, dropPosition: Int): Boolean {
+ return true
+ }
+
+ override fun onItemDragStarted(position: Int) {
+ notifyDataSetChanged()
+ }
+
+ override fun onItemDragFinished(fromPosition: Int, toPosition: Int, result: Boolean) {
+ notifyDataSetChanged()
+ }
+
+ fun setSongToRemove(song: Song) {
+ songToRemove = song
+ }
+
+ inner class ViewHolder(itemView: View) : SongAdapter.ViewHolder(itemView) {
+ @DraggableItemStateFlags
+ private var mDragStateFlags: Int = 0
+
+ override var songMenuRes: Int
+ get() = R.menu.menu_item_playing_queue_song
+ set(value) {
+ super.songMenuRes = value
+ }
+
+ init {
+ dragView?.isVisible = true
+ }
+
+ override fun onClick(v: View?) {
+ if (isInQuickSelectMode) {
+ toggleChecked(layoutPosition)
+ } else {
+ MusicPlayerRemote.playSongAt(layoutPosition)
+ }
+ }
+
+ override fun onSongMenuItemClick(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_remove_from_playing_queue -> {
+ removeFromQueue(layoutPosition)
+ return true
+ }
+ }
+ return super.onSongMenuItemClick(item)
+ }
+
+ @DraggableItemStateFlags
+ override fun getDragStateFlags(): Int {
+ return mDragStateFlags
+ }
+
+ override fun setDragStateFlags(@DraggableItemStateFlags flags: Int) {
+ mDragStateFlags = flags
+ }
+
+ override fun getSwipeableContainerView(): View {
+ return dummyContainer!!
+ }
+ }
+
+ companion object {
+
+ private const val HISTORY = 0
+ private const val CURRENT = 1
+ private const val UP_NEXT = 2
+ }
+
+ override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int): SwipeResultAction {
+ return if (result == SwipeableItemConstants.RESULT_CANCELED) {
+ SwipeResultActionDefault()
+ } else {
+ SwipedResultActionRemoveItem(this, position)
+ }
+ }
+
+ override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int {
+ return if (onCheckCanStartDrag(holder, position, x, y)) {
+ SwipeableItemConstants.REACTION_CAN_NOT_SWIPE_BOTH_H
+ } else {
+ SwipeableItemConstants.REACTION_CAN_SWIPE_BOTH_H
+ }
+ }
+
+ override fun onSwipeItemStarted(holder: ViewHolder, p1: Int) {
+ }
+
+ override fun onSetSwipeBackground(holder: ViewHolder, position: Int, result: Int) {
+ }
+
+ internal class SwipedResultActionRemoveItem(
+ private val adapter: PlayingQueueAdapter,
+ private val position: Int,
+ ) : SwipeResultActionRemoveItem() {
+
+ private var songToRemove: Song? = null
+ override fun onPerformAction() {
+ // currentlyShownSnackbar = null
+ }
+
+ override fun onSlideAnimationEnd() {
+ // initializeSnackBar(adapter, position, activity, isPlaying)
+ songToRemove = adapter.dataSet[position]
+ // 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
+ adapter.setSongToRemove(songToRemove!!)
+ removeFromQueue(songToRemove!!)
+ }
+ }
+}
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
new file mode 100644
index 000000000..82ba1a7f7
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/ShuffleButtonSongAdapter.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.accentColor
+import code.name.monkey.retromusic.extensions.accentOutlineColor
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.RetroUtil
+import com.google.android.material.button.MaterialButton
+
+class ShuffleButtonSongAdapter(
+ activity: FragmentActivity,
+ dataSet: MutableList,
+ itemLayoutRes: Int
+) : AbsOffsetSongAdapter(activity, dataSet, itemLayoutRes) {
+
+
+ override fun createViewHolder(view: View): SongAdapter.ViewHolder {
+ return ViewHolder(view)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0) OFFSET_ITEM else SONG
+ }
+
+ override fun onBindViewHolder(holder: SongAdapter.ViewHolder, position: Int) {
+ if (holder.itemViewType == OFFSET_ITEM) {
+ val viewHolder = holder as ViewHolder
+ viewHolder.playAction?.let {
+ it.setOnClickListener {
+ MusicPlayerRemote.openQueue(dataSet, 0, true)
+ }
+ it.accentOutlineColor()
+ }
+ viewHolder.shuffleAction?.let {
+ it.setOnClickListener {
+ MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
+ }
+ it.accentColor()
+ }
+ } else {
+ super.onBindViewHolder(holder, position - 1)
+ val landscape = RetroUtil.isLandscape
+ if ((PreferenceUtil.songGridSize > 2 && !landscape) || (PreferenceUtil.songGridSizeLand > 5 && landscape)) {
+ holder.menu?.isVisible = false
+ }
+ }
+ }
+
+ inner class ViewHolder(itemView: View) : AbsOffsetSongAdapter.ViewHolder(itemView) {
+ val playAction: MaterialButton? = itemView.findViewById(R.id.playAction)
+ val shuffleAction: MaterialButton? = itemView.findViewById(R.id.shuffleAction)
+
+ override fun onClick(v: View?) {
+ if (itemViewType == OFFSET_ITEM) {
+ MusicPlayerRemote.openAndShuffleQueue(dataSet, true)
+ return
+ }
+ super.onClick(v)
+ }
+ }
+
+}
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
new file mode 100755
index 000000000..f1f50e627
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SimpleSongAdapter.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentActivity
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.util.MusicUtil
+
+class SimpleSongAdapter(
+ context: FragmentActivity,
+ songs: ArrayList,
+ layoutRes: Int
+) : SongAdapter(context, songs, layoutRes) {
+
+ override fun swapDataSet(dataSet: List) {
+ this.dataSet = dataSet.toMutableList()
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false))
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ super.onBindViewHolder(holder, position)
+ val fixedTrackNumber = MusicUtil.getFixedTrackNumber(dataSet[position].trackNumber)
+ val trackAndTime = (if (fixedTrackNumber > 0) "$fixedTrackNumber | " else "") +
+ MusicUtil.getReadableDurationString(dataSet[position].duration)
+
+ holder.time?.text = trackAndTime
+ holder.text2?.text = dataSet[position].artistName
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+}
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
new file mode 100644
index 000000000..7b7b7bf99
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/adapter/song/SongAdapter.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import androidx.navigation.findNavController
+import code.name.monkey.retromusic.EXTRA_ALBUM_ID
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.adapter.base.AbsMultiSelectAdapter
+import code.name.monkey.retromusic.adapter.base.MediaEntryViewHolder
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.RetroMusicColoredTarget
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.helper.SortOrder
+import code.name.monkey.retromusic.helper.menu.SongMenuHelper
+import code.name.monkey.retromusic.helper.menu.SongsMenuHelper
+import code.name.monkey.retromusic.model.Song
+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 me.zhanghai.android.fastscroll.PopupTextProvider
+
+/**
+ * Created by hemanths on 13/08/17.
+ */
+
+open class SongAdapter(
+ override val activity: FragmentActivity,
+ var dataSet: MutableList,
+ protected var itemLayoutRes: Int,
+ showSectionName: Boolean = true
+) : AbsMultiSelectAdapter(
+ activity,
+ R.menu.menu_media_selection
+), PopupTextProvider {
+
+ private var showSectionName = true
+
+ init {
+ this.showSectionName = showSectionName
+ this.setHasStableIds(true)
+ }
+
+ open fun swapDataSet(dataSet: List) {
+ this.dataSet = ArrayList(dataSet)
+ notifyDataSetChanged()
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dataSet[position].id
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view =
+ try {
+ LayoutInflater.from(activity).inflate(itemLayoutRes, parent, false)
+ } catch (e: Resources.NotFoundException) {
+ LayoutInflater.from(activity).inflate(R.layout.item_list, parent, false)
+ }
+ return createViewHolder(view)
+ }
+
+ protected open fun createViewHolder(view: View): ViewHolder {
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val song = dataSet[position]
+ val isChecked = isChecked(song)
+ holder.itemView.isActivated = isChecked
+ holder.menu?.isGone = isChecked
+ holder.title?.text = getSongTitle(song)
+ holder.text?.text = getSongText(song)
+ holder.text2?.text = getSongText(song)
+ loadAlbumCover(song, holder)
+ val landscape = RetroUtil.isLandscape
+ if ((PreferenceUtil.songGridSize > 2 && !landscape) || (PreferenceUtil.songGridSizeLand > 5 && landscape)) {
+ holder.menu?.isVisible = false
+ }
+ }
+
+ private fun setColors(color: MediaNotificationProcessor, holder: ViewHolder) {
+ if (holder.paletteColorContainer != null) {
+ holder.title?.setTextColor(color.primaryTextColor)
+ holder.text?.setTextColor(color.secondaryTextColor)
+ holder.paletteColorContainer?.setBackgroundColor(color.backgroundColor)
+ holder.menu?.imageTintList = ColorStateList.valueOf(color.primaryTextColor)
+ }
+ holder.mask?.backgroundTintList = ColorStateList.valueOf(color.primaryTextColor)
+ }
+
+ protected open fun loadAlbumCover(song: Song, holder: ViewHolder) {
+ if (holder.image == null) {
+ return
+ }
+ Glide.with(activity)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ .into(object : RetroMusicColoredTarget(holder.image!!) {
+ override fun onColorReady(colors: MediaNotificationProcessor) {
+ setColors(colors, holder)
+ }
+ })
+ }
+
+ private fun getSongTitle(song: Song): String {
+ return song.title
+ }
+
+ private fun getSongText(song: Song): String {
+ return song.artistName
+ }
+
+ private fun getSongText2(song: Song): String {
+ return song.albumName
+ }
+
+ override fun getItemCount(): Int {
+ return dataSet.size
+ }
+
+ override fun getIdentifier(position: Int): Song? {
+ return dataSet[position]
+ }
+
+ override fun getName(model: Song): String {
+ return model.title
+ }
+
+ override fun onMultipleItemAction(menuItem: MenuItem, selection: List) {
+ SongsMenuHelper.handleMenuClick(activity, selection, menuItem.itemId)
+ }
+
+ override fun getPopupText(position: Int): String {
+ val sectionName: String? = when (PreferenceUtil.songSortOrder) {
+ SortOrder.SongSortOrder.SONG_DEFAULT -> return MusicUtil.getSectionName(
+ dataSet[position].title,
+ true
+ )
+
+ SortOrder.SongSortOrder.SONG_A_Z, SortOrder.SongSortOrder.SONG_Z_A -> dataSet[position].title
+ SortOrder.SongSortOrder.SONG_ALBUM -> dataSet[position].albumName
+ SortOrder.SongSortOrder.SONG_ARTIST -> dataSet[position].artistName
+ SortOrder.SongSortOrder.SONG_YEAR -> return MusicUtil.getYearString(dataSet[position].year)
+ SortOrder.SongSortOrder.COMPOSER -> dataSet[position].composer
+ SortOrder.SongSortOrder.SONG_ALBUM_ARTIST -> dataSet[position].albumArtist
+ else -> {
+ return ""
+ }
+ }
+ return MusicUtil.getSectionName(sectionName)
+ }
+
+ open inner class ViewHolder(itemView: View) : MediaEntryViewHolder(itemView) {
+ protected open var songMenuRes = SongMenuHelper.MENU_RES
+ protected open val song: Song
+ get() = dataSet[layoutPosition]
+
+ init {
+ menu?.setOnClickListener(object : SongMenuHelper.OnClickSongMenu(activity) {
+ override val song: Song
+ get() = this@ViewHolder.song
+
+ override val menuRes: Int
+ get() = songMenuRes
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ return onSongMenuItemClick(item) || super.onMenuItemClick(item)
+ }
+ })
+ }
+
+ protected open fun onSongMenuItemClick(item: MenuItem): Boolean {
+ if (image != null && image!!.isVisible) {
+ when (item.itemId) {
+ R.id.action_go_to_album -> {
+ activity.findNavController(R.id.fragment_container)
+ .navigate(
+ R.id.albumDetailsFragment,
+ bundleOf(EXTRA_ALBUM_ID to song.albumId)
+ )
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ override fun onClick(v: View?) {
+ if (isInQuickSelectMode) {
+ toggleChecked(layoutPosition)
+ } else {
+ MusicPlayerRemote.openQueue(dataSet, layoutPosition, true)
+ }
+ }
+
+ override fun onLongClick(v: View?): Boolean {
+ println("Long click")
+ return toggleChecked(layoutPosition)
+ }
+ }
+
+ companion object {
+ val TAG: String = SongAdapter::class.java.simpleName
+ }
+}
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
new file mode 100644
index 000000000..ad8f964e7
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutIconGenerator.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.graphics.drawable.Icon
+import android.graphics.drawable.LayerDrawable
+import android.os.Build
+import android.util.TypedValue
+import androidx.annotation.RequiresApi
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.ThemeStore
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.util.PreferenceUtil
+
+@RequiresApi(Build.VERSION_CODES.N_MR1)
+object AppShortcutIconGenerator {
+ fun generateThemedIcon(context: Context, iconId: Int): Icon {
+ return if (PreferenceUtil.isColoredAppShortcuts) {
+ generateUserThemedIcon(context, iconId)
+ } else {
+ generateDefaultThemedIcon(context, iconId)
+ }
+ }
+
+ private fun generateDefaultThemedIcon(context: Context, iconId: Int): Icon {
+ // Return an Icon of iconId with default colors
+ return generateThemedIcon(
+ context,
+ iconId,
+ context.getColor(R.color.app_shortcut_default_foreground),
+ context.getColor(R.color.app_shortcut_default_background)
+ )
+ }
+
+ private fun generateUserThemedIcon(context: Context, iconId: Int): Icon {
+ // Get background color from context's theme
+ val typedColorBackground = TypedValue()
+ context.theme.resolveAttribute(android.R.attr.colorBackground, typedColorBackground, true)
+
+ // Return an Icon of iconId with those colors
+ return generateThemedIcon(
+ context, iconId, ThemeStore.accentColor(context), typedColorBackground.data
+ )
+ }
+
+ private fun generateThemedIcon(
+ context: Context,
+ iconId: Int,
+ foregroundColor: Int,
+ backgroundColor: Int,
+ ): Icon {
+ // Get and tint foreground and background drawables
+ val vectorDrawable = context.getTintedDrawable(iconId, foregroundColor)
+ val backgroundDrawable =
+ context.getTintedDrawable(R.drawable.ic_app_shortcut_background, backgroundColor)
+
+ // Squash the two drawables together
+ val layerDrawable = LayerDrawable(arrayOf(backgroundDrawable, vectorDrawable))
+
+ // Return as an Icon
+ return Icon.createWithBitmap(layerDrawable.toBitmap())
+ }
+}
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
new file mode 100644
index 000000000..9f10d795b
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.TopTracksShortcutType
+import code.name.monkey.retromusic.extensions.extraNotNull
+import code.name.monkey.retromusic.model.Playlist
+import code.name.monkey.retromusic.model.smartplaylist.LastAddedPlaylist
+import code.name.monkey.retromusic.model.smartplaylist.ShuffleAllPlaylist
+import code.name.monkey.retromusic.model.smartplaylist.TopTracksPlaylist
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PLAY_PLAYLIST
+import code.name.monkey.retromusic.service.MusicService.Companion.INTENT_EXTRA_PLAYLIST
+import code.name.monkey.retromusic.service.MusicService.Companion.INTENT_EXTRA_SHUFFLE_MODE
+import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_NONE
+import code.name.monkey.retromusic.service.MusicService.Companion.SHUFFLE_MODE_SHUFFLE
+
+class AppShortcutLauncherActivity : Activity() {
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ when (extraNotNull(KEY_SHORTCUT_TYPE, SHORTCUT_TYPE_NONE).value) {
+ SHORTCUT_TYPE_SHUFFLE_ALL -> {
+ startServiceWithPlaylist(
+ SHUFFLE_MODE_SHUFFLE, ShuffleAllPlaylist()
+ )
+ DynamicShortcutManager.reportShortcutUsed(this, ShuffleAllShortcutType.id)
+ }
+ SHORTCUT_TYPE_TOP_TRACKS -> {
+ startServiceWithPlaylist(
+ SHUFFLE_MODE_NONE, TopTracksPlaylist()
+ )
+ DynamicShortcutManager.reportShortcutUsed(this, TopTracksShortcutType.id)
+ }
+ SHORTCUT_TYPE_LAST_ADDED -> {
+ startServiceWithPlaylist(
+ SHUFFLE_MODE_NONE, LastAddedPlaylist()
+ )
+ DynamicShortcutManager.reportShortcutUsed(this, LastAddedShortcutType.id)
+ }
+ }
+ finish()
+ }
+
+ private fun startServiceWithPlaylist(shuffleMode: Int, playlist: Playlist) {
+ val intent = Intent(this, MusicService::class.java)
+ intent.action = ACTION_PLAY_PLAYLIST
+
+ val bundle = bundleOf(
+ INTENT_EXTRA_PLAYLIST to playlist,
+ INTENT_EXTRA_SHUFFLE_MODE to shuffleMode
+ )
+
+ intent.putExtras(bundle)
+
+ startService(intent)
+ }
+
+ companion object {
+ const val KEY_SHORTCUT_TYPE = "io.github.muntashirakon.Music.appshortcuts.ShortcutType"
+ const val SHORTCUT_TYPE_SHUFFLE_ALL = 0L
+ const val SHORTCUT_TYPE_TOP_TRACKS = 1L
+ const val SHORTCUT_TYPE_LAST_ADDED = 2L
+ const val SHORTCUT_TYPE_NONE = 4L
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt
new file mode 100644
index 000000000..a943ed0c8
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/DynamicShortcutManager.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Build
+import androidx.core.content.getSystemService
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType
+import code.name.monkey.retromusic.appshortcuts.shortcuttype.TopTracksShortcutType
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class DynamicShortcutManager(private val context: Context) {
+ private val shortcutManager: ShortcutManager? =
+ this.context.getSystemService()
+
+ private val defaultShortcuts: List
+ get() = listOf(
+ ShuffleAllShortcutType(context).shortcutInfo,
+ TopTracksShortcutType(context).shortcutInfo,
+ LastAddedShortcutType(context).shortcutInfo
+ )
+
+ fun initDynamicShortcuts() {
+ // if (shortcutManager.dynamicShortcuts.size == 0) {
+ shortcutManager?.dynamicShortcuts = defaultShortcuts
+ // }
+ }
+
+ fun updateDynamicShortcuts() {
+ shortcutManager?.updateShortcuts(defaultShortcuts)
+ }
+
+ companion object {
+
+ fun createShortcut(
+ context: Context,
+ id: String,
+ shortLabel: String,
+ longLabel: String,
+ icon: Icon,
+ intent: Intent
+ ): ShortcutInfo {
+ return ShortcutInfo.Builder(context, id)
+ .setShortLabel(shortLabel)
+ .setLongLabel(longLabel)
+ .setIcon(icon)
+ .setIntent(intent)
+ .build()
+ }
+
+ fun reportShortcutUsed(context: Context, shortcutId: String) {
+ context.getSystemService()?.reportShortcutUsed(shortcutId)
+ }
+ }
+}
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
new file mode 100644
index 000000000..4b0c7f3a0
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/BaseShortcutType.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.os.Build
+import androidx.core.os.bundleOf
+import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+abstract class BaseShortcutType(internal var context: Context) {
+
+ internal abstract val shortcutInfo: ShortcutInfo
+
+ /**
+ * Creates an Intent that will launch MainActivtiy and immediately play {@param songs} in either shuffle or normal mode
+ *
+ * @param shortcutType Describes the type of shortcut to create (ShuffleAll, TopTracks, custom playlist, etc.)
+ * @return
+ */
+ internal fun getPlaySongsIntent(shortcutType: Long): Intent {
+ val intent = Intent(context, AppShortcutLauncherActivity::class.java)
+ intent.action = Intent.ACTION_VIEW
+ val b = bundleOf(AppShortcutLauncherActivity.KEY_SHORTCUT_TYPE to shortcutType)
+ intent.putExtras(b)
+ return intent
+ }
+
+ companion object {
+ internal const val ID_PREFIX = "io.github.muntashirakon.Music.appshortcuts.id."
+ val id: String
+ get() = ID_PREFIX + "invalid"
+ }
+}
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
new file mode 100644
index 000000000..d0976042c
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/LastAddedShortcutType.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.os.Build
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
+import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class LastAddedShortcutType(context: Context) : BaseShortcutType(context) {
+
+ override val shortcutInfo: ShortcutInfo
+ get() = ShortcutInfo.Builder(
+ context,
+ id
+ ).setShortLabel(context.getString(R.string.app_shortcut_last_added_short)).setLongLabel(
+ context.getString(R.string.app_shortcut_last_added_long)
+ ).setIcon(
+ AppShortcutIconGenerator.generateThemedIcon(
+ context,
+ R.drawable.ic_app_shortcut_last_added
+ )
+ ).setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_LAST_ADDED))
+ .build()
+
+ companion object {
+
+ val id: String
+ get() = ID_PREFIX + "last_added"
+ }
+}
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
new file mode 100644
index 000000000..21855cacd
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/ShuffleAllShortcutType.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.os.Build
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
+import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class ShuffleAllShortcutType(context: Context) : BaseShortcutType(context) {
+
+ override val shortcutInfo: ShortcutInfo
+ get() = ShortcutInfo.Builder(context, id)
+ .setShortLabel(context.getString(R.string.app_shortcut_shuffle_all_short))
+ .setLongLabel(context.getString(R.string.app_shortcut_shuffle_all_long))
+ .setIcon(AppShortcutIconGenerator.generateThemedIcon(context, R.drawable.ic_app_shortcut_shuffle_all))
+ .setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_SHUFFLE_ALL))
+ .build()
+
+ companion object {
+
+ val id: String
+ get() = ID_PREFIX + "shuffle_all"
+ }
+}
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
new file mode 100644
index 000000000..fc0fa9f47
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/shortcuttype/TopTracksShortcutType.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.os.Build
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.appshortcuts.AppShortcutIconGenerator
+import code.name.monkey.retromusic.appshortcuts.AppShortcutLauncherActivity
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class TopTracksShortcutType(context: Context) : BaseShortcutType(context) {
+
+ override val shortcutInfo: ShortcutInfo
+ get() = ShortcutInfo.Builder(
+ context, id
+ ).setShortLabel(context.getString(R.string.app_shortcut_top_tracks_short)).setLongLabel(
+ context.getString(R.string.app_shortcut_top_tracks_long)
+ ).setIcon(
+ AppShortcutIconGenerator.generateThemedIcon(
+ context, R.drawable.ic_app_shortcut_top_tracks
+ )
+ ).setIntent(getPlaySongsIntent(AppShortcutLauncherActivity.SHORTCUT_TYPE_TOP_TRACKS))
+ .build()
+
+ companion object {
+
+ val id: String
+ get() = ID_PREFIX + "top_tracks"
+ }
+}
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
new file mode 100644
index 000000000..7d9497209
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.PreferenceUtil
+import code.name.monkey.retromusic.util.RetroUtil
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+
+class AppWidgetBig : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(
+ context.packageName, R.layout.app_widget_big
+ )
+
+ appWidgetView.setViewVisibility(
+ R.id.media_titles,
+ View.INVISIBLE
+ )
+ appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next, context.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ MaterialValueHelper.getPrimaryTextColor(context, false)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ MaterialValueHelper.getPrimaryTextColor(context, false)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow_white_32dp,
+ MaterialValueHelper.getPrimaryTextColor(context, false)
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(
+ service.packageName, R.layout.app_widget_big
+ )
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(
+ R.id.media_titles,
+ View.INVISIBLE
+ )
+ } else {
+ appWidgetView.setViewVisibility(
+ R.id.media_titles,
+ View.VISIBLE
+ )
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(
+ R.id.text,
+ getSongArtistAndAlbum(song)
+ )
+ }
+
+ val primaryColor = MaterialValueHelper.getPrimaryTextColor(service, false)
+ // Set correct drawable for pause state
+ val playPauseRes =
+ if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes,
+ primaryColor
+ ).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ primaryColor
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ primaryColor
+ ).toBitmap()
+ )
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ // Load the album cover async and push the update on completion
+ val p = RetroUtil.getScreenSize(service)
+ val widgetImageSize = p.x.coerceAtMost(p.y)
+ val appContext = service.applicationContext
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(appContext)
+ .asBitmap()
+ //.checkIgnoreMediaStore()
+ .load(RetroGlideExtension.getSongModel(song))
+ .into(object : CustomTarget(widgetImageSize, widgetImageSize) {
+ override fun onResourceReady(
+ resource: Bitmap,
+ transition: Transition?,
+ ) {
+ update(resource)
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null)
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {}
+
+ private fun update(bitmap: Bitmap?) {
+ if (bitmap == null) {
+ appWidgetView.setImageViewResource(
+ R.id.image,
+ R.drawable.default_audio_art
+ )
+ } else {
+ appWidgetView.setImageViewBitmap(R.id.image, bitmap)
+ }
+ pushUpdate(appContext, appWidgetIds, appWidgetView)
+ }
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent =
+ PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.clickable_area, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME: String = "app_widget_big"
+ private var mInstance: AppWidgetBig? = null
+
+ val instance: AppWidgetBig
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetBig()
+ }
+ 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
new file mode 100644
index 000000000..471d908d6
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.PreferenceUtil
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+
+class AppWidgetCard : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_card)
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
+ val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ secondaryColor
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ secondaryColor
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow_white_32dp,
+ secondaryColor
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_card)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ } else {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
+ }
+
+ // Set correct drawable for pause state
+ val playPauseRes =
+ if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ if (imageSize == 0) {
+ imageSize =
+ service.resources.getDimensionPixelSize(R.dimen.app_widget_card_image_size)
+ }
+ if (cardRadius == 0f) {
+ cardRadius =
+ service.resources.getDimension(R.dimen.app_widget_card_radius)
+ }
+
+ // Load the album cover async and push the update on completion
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(service)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ .centerCrop()
+ .into(object : CustomTarget(imageSize, imageSize) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?,
+ ) {
+ val palette = resource.palette
+ update(
+ resource.bitmap, palette.getVibrantColor(
+ palette.getMutedColor(
+ MaterialValueHelper.getSecondaryTextColor(
+ service, true
+ )
+ )
+ )
+ )
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {}
+
+ private fun update(bitmap: Bitmap?, color: Int) {
+ // Set correct drawable for pause state
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(playPauseRes, color).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
+ )
+
+ val image = getAlbumArtDrawable(service, bitmap)
+ val roundedBitmap = createRoundedBitmap(
+ image, imageSize, imageSize, cardRadius, 0F, cardRadius, 0F
+ )
+ appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
+
+ pushUpdate(service, appWidgetIds, appWidgetView)
+ }
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent =
+ PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME = "app_widget_card"
+
+ private var mInstance: AppWidgetCard? = null
+ private var imageSize = 0
+ private var cardRadius = 0f
+
+ val instance: AppWidgetCard
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetCard()
+ }
+ return mInstance!!
+ }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCircle.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCircle.kt
new file mode 100644
index 000000000..cb3ae0e44
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCircle.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.service.MusicService.Companion.TOGGLE_FAVORITE
+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.Glide
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+
+class AppWidgetCircle : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_circle)
+
+ appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
+ val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow,
+ secondaryColor
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_circle)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set correct drawable for pause state
+ val playPauseRes =
+ if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+ val isFavorite = runBlocking(Dispatchers.IO) {
+ return@runBlocking MusicUtil.isFavorite(song)
+ }
+ val favoriteRes =
+ if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_favorite,
+ service.getTintedDrawable(
+ favoriteRes,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ if (imageSize == 0) {
+ val p = RetroUtil.getScreenSize(service)
+ imageSize = p.x.coerceAtMost(p.y)
+ }
+
+ // Load the album cover async and push the update on completion
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(service)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ .apply(RequestOptions.circleCropTransform())
+ .into(object : CustomTarget(imageSize, imageSize) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?,
+ ) {
+ val palette = resource.palette
+ update(
+ resource.bitmap, palette.getVibrantColor(
+ palette.getMutedColor(
+ MaterialValueHelper.getSecondaryTextColor(
+ service, true
+ )
+ )
+ )
+ )
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
+ }
+
+ private fun update(bitmap: Bitmap?, color: Int) {
+ // Set correct drawable for pause state
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes, color
+ ).toBitmap()
+ )
+
+ // Set favorite button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_favorite,
+ service.getTintedDrawable(
+ favoriteRes, color
+ ).toBitmap()
+ )
+ if (bitmap != null) {
+ appWidgetView.setImageViewBitmap(R.id.image, bitmap)
+ }
+
+ pushUpdate(service, appWidgetIds, appWidgetView)
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {}
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent =
+ PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ // Favorite track
+ pendingIntent = buildPendingIntent(context, TOGGLE_FAVORITE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_favorite, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME = "app_widget_circle"
+
+ private var mInstance: AppWidgetCircle? = null
+ private var imageSize = 0
+
+ val instance: AppWidgetCircle
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetCircle()
+ }
+ return mInstance!!
+ }
+ }
+}
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
new file mode 100644
index 000000000..ad04f8aae
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.PreferenceUtil
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+
+class AppWidgetClassic : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ 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(
+ R.id.button_next,
+
+ context.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+
+ context.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow_white_32dp,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_classic)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ } else {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ if (imageSize == 0) {
+ imageSize =
+ service.resources.getDimensionPixelSize(R.dimen.app_widget_classic_image_size)
+ }
+ if (cardRadius == 0f) {
+ cardRadius = service.resources.getDimension(R.dimen.app_widget_card_radius)
+ }
+
+ // Load the album cover async and push the update on completion
+ val appContext = service.applicationContext
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(service)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ //.checkIgnoreMediaStore()
+ .centerCrop()
+ .into(object : CustomTarget(imageSize, imageSize) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?,
+ ) {
+ val palette = resource.palette
+ update(
+ resource.bitmap,
+ palette.getVibrantColor(
+ palette.getMutedColor(
+ MaterialValueHelper.getSecondaryTextColor(
+ service,
+ true
+ )
+ )
+ )
+ )
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null, Color.WHITE)
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {}
+
+ private fun update(bitmap: Bitmap?, color: Int) {
+ // Set correct drawable for pause state
+ val playPauseRes =
+ if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes,
+ color
+ ).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ color
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ color
+ ).toBitmap()
+ )
+
+ val image = getAlbumArtDrawable(service, bitmap)
+ val roundedBitmap =
+ createRoundedBitmap(
+ image,
+ imageSize,
+ imageSize,
+ cardRadius,
+ 0F,
+ cardRadius,
+ 0F
+ )
+ appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
+
+ pushUpdate(appContext, appWidgetIds, appWidgetView)
+ }
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent = PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME = "app_widget_classic"
+
+ private var mInstance: AppWidgetClassic? = null
+ private var imageSize = 0
+ private var cardRadius = 0f
+
+ val instance: AppWidgetClassic
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetClassic()
+ }
+ return mInstance!!
+ }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetMD3.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetMD3.kt
new file mode 100644
index 000000000..618914ff3
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetMD3.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.DensityUtil
+import code.name.monkey.retromusic.util.PreferenceUtil
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+
+class AppWidgetMD3 : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_md3)
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
+ val secondaryColor = MaterialValueHelper.getSecondaryTextColor(context, true)
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ secondaryColor
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ secondaryColor
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow_white_32dp,
+ secondaryColor
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_md3)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ } else {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(R.id.text, getSongArtistAndAlbum(song))
+ }
+
+ // Set correct drawable for pause state
+ val playPauseRes =
+ if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow_white_32dp
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(
+ playPauseRes,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ MaterialValueHelper.getSecondaryTextColor(service, true)
+ ).toBitmap()
+ )
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ if (imageSize == 0) {
+ imageSize =
+ service.resources.getDimensionPixelSize(R.dimen.app_widget_card_image_size)
+ }
+ if (cardRadius == 0f) {
+ cardRadius =
+ DensityUtil.dip2px(service, 8F).toFloat()
+ }
+
+ // Load the album cover async and push the update on completion
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(service)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ .load(RetroGlideExtension.getSongModel(song))
+ .centerCrop()
+ .into(object : CustomTarget(imageSize, imageSize) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?,
+ ) {
+ val palette = resource.palette
+ update(
+ resource.bitmap, palette.getVibrantColor(
+ palette.getMutedColor(
+ MaterialValueHelper.getSecondaryTextColor(
+ service, true
+ )
+ )
+ )
+ )
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {}
+
+ private fun update(bitmap: Bitmap?, color: Int) {
+ // Set correct drawable for pause state
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(playPauseRes, color).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
+ )
+
+ val image = getAlbumArtDrawable(service, bitmap)
+ val roundedBitmap = createRoundedBitmap(
+ image,
+ imageSize,
+ imageSize,
+ cardRadius,
+ cardRadius,
+ cardRadius,
+ cardRadius
+ )
+ appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
+
+ pushUpdate(service, appWidgetIds, appWidgetView)
+ }
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent =
+ PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME = "app_widget_md3"
+
+ private var mInstance: AppWidgetMD3? = null
+ private var imageSize = 0
+ private var cardRadius = 0F
+
+ val instance: AppWidgetMD3
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetMD3()
+ }
+ return mInstance!!
+ }
+ }
+}
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
new file mode 100644
index 000000000..ee4e87e99
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.MaterialValueHelper
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.glide.RetroGlideExtension
+import code.name.monkey.retromusic.glide.RetroGlideExtension.asBitmapPalette
+import code.name.monkey.retromusic.glide.RetroGlideExtension.songCoverOptions
+import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.PreferenceUtil
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.Target
+import com.bumptech.glide.request.transition.Transition
+
+class AppWidgetSmall : BaseAppWidget() {
+ private var target: Target? = null // for cancellation
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on default click and hide
+ * actions if service not running.
+ */
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_small)
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ appWidgetView.setImageViewResource(R.id.image, R.drawable.default_audio_art)
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ context.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+
+ context.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+
+ context.getTintedDrawable(
+ R.drawable.ic_play_arrow_white_32dp,
+ MaterialValueHelper.getSecondaryTextColor(context, true)
+ ).toBitmap()
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_small)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ } else {
+ if (song.title.isEmpty() || song.artistName.isEmpty()) {
+ appWidgetView.setTextViewText(R.id.text_separator, "")
+ } else {
+ appWidgetView.setTextViewText(R.id.text_separator, "•")
+ }
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(R.id.text, song.artistName)
+ }
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ if (imageSize == 0) {
+ imageSize = service.resources.getDimensionPixelSize(R.dimen.app_widget_small_image_size)
+ }
+ if (cardRadius == 0f) {
+ cardRadius = service.resources.getDimension(R.dimen.app_widget_card_radius)
+ }
+
+ // Load the album cover async and push the update on completion
+ val appContext = service.applicationContext
+ service.runOnUiThread {
+ if (target != null) {
+ Glide.with(service).clear(target)
+ }
+ target = Glide.with(service)
+ .asBitmapPalette()
+ .songCoverOptions(song)
+ //.checkIgnoreMediaStore()
+ .load(RetroGlideExtension.getSongModel(song))
+ .centerCrop()
+ .into(object : CustomTarget(imageSize, imageSize) {
+ override fun onResourceReady(
+ resource: BitmapPaletteWrapper,
+ transition: Transition?,
+ ) {
+ val palette = resource.palette
+ update(
+ resource.bitmap, palette.getVibrantColor(
+ palette.getMutedColor(
+ MaterialValueHelper.getSecondaryTextColor(
+ service, true
+ )
+ )
+ )
+ )
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ super.onLoadFailed(errorDrawable)
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true))
+ }
+
+ private fun update(bitmap: Bitmap?, color: Int) {
+ // Set correct drawable for pause state
+ val playPauseRes = if (isPlaying) R.drawable.ic_pause
+ else R.drawable.ic_play_arrow_white_32dp
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(playPauseRes, color).toBitmap()
+ )
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(R.drawable.ic_skip_next, color).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(R.drawable.ic_skip_previous, color).toBitmap()
+ )
+
+ val image = getAlbumArtDrawable(service, bitmap)
+ val roundedBitmap = createRoundedBitmap(
+ image, imageSize, imageSize, cardRadius, 0f, 0f, 0f
+ )
+ appWidgetView.setImageViewBitmap(R.id.image, roundedBitmap)
+
+ pushUpdate(appContext, appWidgetIds, appWidgetView)
+ }
+ })
+ }
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent =
+ PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ companion object {
+
+ const val NAME: String = "app_widget_small"
+
+ private var mInstance: AppWidgetSmall? = null
+ private var imageSize = 0
+ private var cardRadius = 0f
+
+ val instance: AppWidgetSmall
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetSmall()
+ }
+ return mInstance!!
+ }
+ }
+}
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
new file mode 100644
index 000000000..b7abb6ee5
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.activities.MainActivity
+import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget
+import code.name.monkey.retromusic.extensions.getTintedDrawable
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+import code.name.monkey.retromusic.util.PreferenceUtil
+
+class AppWidgetText : BaseAppWidget() {
+ override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) {
+ val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_text)
+
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ context.getTintedDrawable(R.drawable.ic_skip_next, ContextCompat.getColor(
+ context, code.name.monkey.appthemehelper.R.color.md_white_1000
+ )).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ context.getTintedDrawable(R.drawable.ic_skip_previous, ContextCompat.getColor(
+ context, code.name.monkey.appthemehelper.R.color.md_white_1000
+ )
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ context.getTintedDrawable(R.drawable.ic_play_arrow_white_32dp, ContextCompat.getColor(
+ context, code.name.monkey.appthemehelper.R.color.md_white_1000
+ )
+ ).toBitmap()
+ )
+
+ appWidgetView.setTextColor(
+ R.id.title, ContextCompat.getColor(context, code.name.monkey.appthemehelper.R.color.md_white_1000)
+ )
+ appWidgetView.setTextColor(
+ R.id.text, ContextCompat.getColor(context, code.name.monkey.appthemehelper.R.color.md_white_1000)
+ )
+
+ linkButtons(context, appWidgetView)
+ pushUpdate(context, appWidgetIds, appWidgetView)
+ }
+
+ /**
+ * 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,
+ PreferenceUtil.isExpandPanel
+ )
+
+ val serviceName = ComponentName(context, MusicService::class.java)
+
+ // Home
+ action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ var pendingIntent = PendingIntent.getActivity(
+ context, 0, action, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ views.setOnClickPendingIntent(R.id.image, pendingIntent)
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent)
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent)
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent)
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent)
+ }
+
+ override fun performUpdate(service: MusicService, appWidgetIds: IntArray?) {
+ val appWidgetView = RemoteViews(service.packageName, R.layout.app_widget_text)
+
+ val isPlaying = service.isPlaying
+ val song = service.currentSong
+
+ // Set the titles and artwork
+ if (song.title.isEmpty() && song.artistName.isEmpty()) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE)
+ } else {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE)
+ appWidgetView.setTextViewText(R.id.title, song.title)
+ appWidgetView.setTextViewText(R.id.text, song.artistName)
+ }
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView)
+
+ // Set correct drawable for pause state
+ val playPauseRes = if (isPlaying) R.drawable.ic_pause
+ else R.drawable.ic_play_arrow_white_32dp
+ appWidgetView.setImageViewBitmap(
+ R.id.button_toggle_play_pause,
+ service.getTintedDrawable(playPauseRes, ContextCompat.getColor(
+ service, code.name.monkey.appthemehelper.R.color.md_white_1000)
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_next,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_next,
+ ContextCompat.getColor(
+ service,
+ code.name.monkey.appthemehelper.R.color.md_white_1000
+ )
+ ).toBitmap()
+ )
+ appWidgetView.setImageViewBitmap(
+ R.id.button_prev,
+ service.getTintedDrawable(
+ R.drawable.ic_skip_previous,
+ ContextCompat.getColor(
+ service, code.name.monkey.appthemehelper.R.color.md_white_1000
+ )
+ ).toBitmap()
+ )
+
+ pushUpdate(service.applicationContext, appWidgetIds, appWidgetView)
+ }
+
+ companion object {
+
+ const val NAME: String = "app_widget_text"
+
+ private var mInstance: AppWidgetText? = null
+
+ val instance: AppWidgetText
+ @Synchronized get() {
+ if (mInstance == null) {
+ mInstance = AppWidgetText()
+ }
+ return mInstance!!
+ }
+ }
+}
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
new file mode 100644
index 000000000..079e90ae6
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/BootReceiver.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import code.name.monkey.retromusic.service.MusicService
+
+class BootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val widgetManager = AppWidgetManager.getInstance(context)
+
+ // Start music service if there are any existing widgets
+ if (widgetManager.getAppWidgetIds(
+ ComponentName(
+ context, AppWidgetBig::class.java
+ )
+ ).isNotEmpty() || widgetManager.getAppWidgetIds(
+ ComponentName(
+ context, AppWidgetClassic::class.java
+ )
+ ).isNotEmpty() || widgetManager.getAppWidgetIds(
+ ComponentName(
+ context, AppWidgetSmall::class.java
+ )
+ ).isNotEmpty() || widgetManager.getAppWidgetIds(
+ ComponentName(
+ context, AppWidgetCard::class.java
+ )
+ ).isNotEmpty()
+ ) {
+ val serviceIntent = Intent(context, MusicService::class.java)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { // not allowed on Oreo
+ context.startService(serviceIntent)
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..e460b7956
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.*
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.widget.RemoteViews
+import androidx.core.content.ContextCompat
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.service.MusicService.Companion.APP_WIDGET_UPDATE
+import code.name.monkey.retromusic.service.MusicService.Companion.EXTRA_APP_WIDGET_NAME
+import code.name.monkey.retromusic.service.MusicService.Companion.FAVORITE_STATE_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.META_CHANGED
+import code.name.monkey.retromusic.service.MusicService.Companion.PLAY_STATE_CHANGED
+
+abstract class BaseAppWidget : AppWidgetProvider() {
+
+ /**
+ * {@inheritDoc}
+ */
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ defaultAppWidget(context, appWidgetIds)
+ val updateIntent = Intent(APP_WIDGET_UPDATE)
+ updateIntent.putExtra(EXTRA_APP_WIDGET_NAME, NAME)
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
+ updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ context.sendBroadcast(updateIntent)
+ }
+
+ /**
+ * Handle a change notification coming over from [MusicService]
+ */
+ fun notifyChange(service: MusicService, what: String) {
+ if (hasInstances(service)) {
+ if (META_CHANGED == what || PLAY_STATE_CHANGED == what || FAVORITE_STATE_CHANGED == what) {
+ performUpdate(service, null)
+ }
+ }
+ }
+
+ protected fun pushUpdate(
+ context: Context,
+ appWidgetIds: IntArray?,
+ views: RemoteViews
+ ) {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views)
+ } else {
+ appWidgetManager.updateAppWidget(ComponentName(context, javaClass), views)
+ }
+ }
+
+ /**
+ * Check against [AppWidgetManager] if there are any instances of this widget.
+ */
+ private fun hasInstances(context: Context): Boolean {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val mAppWidgetIds = appWidgetManager.getAppWidgetIds(
+ ComponentName(
+ context, javaClass
+ )
+ )
+ return mAppWidgetIds.isNotEmpty()
+ }
+
+ protected fun buildPendingIntent(
+ context: Context,
+ action: String,
+ serviceName: ComponentName
+ ): PendingIntent {
+ val intent = Intent(action)
+ intent.component = serviceName
+ return if (VersionUtils.hasOreo()) {
+ PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+ } else {
+ PendingIntent.getService(
+ context, 0, intent, if (VersionUtils.hasMarshmallow())
+ PendingIntent.FLAG_IMMUTABLE
+ else 0
+ )
+ }
+ }
+
+ protected abstract fun defaultAppWidget(context: Context, appWidgetIds: IntArray)
+
+ abstract fun performUpdate(service: MusicService, appWidgetIds: IntArray?)
+
+ protected fun getAlbumArtDrawable(context: Context, bitmap: Bitmap?): Drawable {
+ return if (bitmap == null) {
+ ContextCompat.getDrawable(context, R.drawable.default_audio_art)!!
+ } else {
+ BitmapDrawable(context.resources, bitmap)
+ }
+ }
+
+ protected fun getSongArtistAndAlbum(song: Song): String {
+ val builder = StringBuilder()
+ builder.append(song.artistName)
+ if (song.artistName.isNotEmpty() && song.albumName.isNotEmpty()) {
+ builder.append(" • ")
+ }
+ builder.append(song.albumName)
+ return builder.toString()
+ }
+
+ companion object {
+
+ const val NAME: String = "app_widget"
+
+ fun createRoundedBitmap(
+ drawable: Drawable?,
+ width: Int,
+ height: Int,
+ tl: Float,
+ tr: Float,
+ bl: Float,
+ br: Float
+ ): Bitmap? {
+ if (drawable == null) {
+ return null
+ }
+
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val c = Canvas(bitmap)
+ drawable.setBounds(0, 0, width, height)
+ drawable.draw(c)
+
+ val rounded = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+
+ val canvas = Canvas(rounded)
+ val paint = Paint()
+ paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ paint.isAntiAlias = true
+ canvas.drawPath(
+ composeRoundedRectPath(
+ RectF(0f, 0f, width.toFloat(), height.toFloat()), tl, tr, bl, br
+ ), paint
+ )
+
+ return rounded
+ }
+
+ protected fun composeRoundedRectPath(
+ rect: RectF,
+ tl: Float,
+ tr: Float,
+ bl: Float,
+ br: Float
+ ): Path {
+ val path = Path()
+ path.moveTo(rect.left + tl, rect.top)
+ path.lineTo(rect.right - tr, rect.top)
+ path.quadTo(rect.right, rect.top, rect.right, rect.top + tr)
+ path.lineTo(rect.right, rect.bottom - br)
+ path.quadTo(rect.right, rect.bottom, rect.right - br, rect.bottom)
+ path.lineTo(rect.left + bl, rect.bottom)
+ path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - bl)
+ path.lineTo(rect.left, rect.top + tl)
+ path.quadTo(rect.left, rect.top, rect.left + tl, rect.top)
+ path.close()
+
+ return path
+ }
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java
new file mode 100644
index 000000000..3d1f4815d
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+
+package code.name.monkey.retromusic.auto;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Created by Beesham Sarendranauth (Beesham)
+ */
+public class AutoMediaIDHelper {
+
+ // Media IDs used on browseable items of MediaBrowser
+ public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY_ROOT__";
+ public static final String MEDIA_ID_ROOT = "__ROOT__";
+ public static final String MEDIA_ID_MUSICS_BY_SEARCH = "__BY_SEARCH__"; // TODO
+ public static final String MEDIA_ID_MUSICS_BY_HISTORY = "__BY_HISTORY__";
+ public static final String MEDIA_ID_MUSICS_BY_TOP_TRACKS = "__BY_TOP_TRACKS__";
+ public static final String MEDIA_ID_MUSICS_BY_SUGGESTIONS = "__BY_SUGGESTIONS__";
+ public static final String MEDIA_ID_MUSICS_BY_PLAYLIST = "__BY_PLAYLIST__";
+ public static final String MEDIA_ID_MUSICS_BY_ALBUM = "__BY_ALBUM__";
+ public static final String MEDIA_ID_MUSICS_BY_ARTIST = "__BY_ARTIST__";
+ public static final String MEDIA_ID_MUSICS_BY_ALBUM_ARTIST = "__BY_ALBUM_ARTIST__";
+ public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__";
+ public static final String MEDIA_ID_MUSICS_BY_SHUFFLE = "__BY_SHUFFLE__";
+ public static final String MEDIA_ID_MUSICS_BY_QUEUE = "__BY_QUEUE__";
+ public static final String RECENT_ROOT = "__RECENT__";
+
+ private static final String CATEGORY_SEPARATOR = "__/__";
+ private static final String LEAF_SEPARATOR = "__|__";
+
+ /**
+ * Create a String value that represents a playable or a browsable media.
+ *
+ * Encode the media browseable categories, if any, and the unique music ID, if any,
+ * into a single String mediaID.
+ *
+ * MediaIDs are of the form __/____|__, to make it
+ * easy to find the category (like genre) that a music was selected from, so we
+ * can correctly build the playing queue. This is specially useful when
+ * one music can appear in more than one list, like "by genre -> genre_1"
+ * and "by artist -> artist_1".
+ *
+ * @param mediaID Unique ID for playable items, or null for browseable items.
+ * @param categories Hierarchy of categories representing this item's browsing parents.
+ * @return A hierarchy-aware media ID.
+ */
+ public static String createMediaID(String mediaID, String... categories) {
+ StringBuilder sb = new StringBuilder();
+ if (categories != null) {
+ for (int i = 0; i < categories.length; i++) {
+ if (!isValidCategory(categories[i])) {
+ throw new IllegalArgumentException("Invalid category: " + categories[i]);
+ }
+ sb.append(categories[i]);
+ if (i < categories.length - 1) {
+ sb.append(CATEGORY_SEPARATOR);
+ }
+ }
+ }
+ if (mediaID != null) {
+ sb.append(LEAF_SEPARATOR).append(mediaID);
+ }
+ return sb.toString();
+ }
+
+ public static String extractCategory(@NonNull String mediaID) {
+ int pos = mediaID.indexOf(LEAF_SEPARATOR);
+ if (pos >= 0) {
+ return mediaID.substring(0, pos);
+ }
+ return mediaID;
+ }
+
+ public static String extractMusicID(@NonNull String mediaID) {
+ int pos = mediaID.indexOf(LEAF_SEPARATOR);
+ if (pos >= 0) {
+ return mediaID.substring(pos + LEAF_SEPARATOR.length());
+ }
+ return null;
+ }
+
+ public static boolean isBrowseable(@NonNull String mediaID) {
+ return !mediaID.contains(LEAF_SEPARATOR);
+ }
+
+ private static boolean isValidCategory(String category) {
+ return category == null ||
+ (!category.contains(CATEGORY_SEPARATOR) && !category.contains(LEAF_SEPARATOR));
+ }
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.kt b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.kt
new file mode 100644
index 000000000..b6e267ed2
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+package code.name.monkey.retromusic.auto
+
+import android.content.Context
+import android.content.res.Resources
+import android.support.v4.media.MediaBrowserCompat
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
+import code.name.monkey.retromusic.model.CategoryInfo
+import code.name.monkey.retromusic.model.Song
+import code.name.monkey.retromusic.repository.*
+import code.name.monkey.retromusic.service.MusicService
+import code.name.monkey.retromusic.util.MusicUtil
+import code.name.monkey.retromusic.util.PreferenceUtil
+import java.lang.ref.WeakReference
+
+
+/**
+ * Created by Beesham Sarendranauth (Beesham)
+ */
+class AutoMusicProvider(
+ private val mContext: Context,
+ private val songsRepository: SongRepository,
+ private val albumsRepository: AlbumRepository,
+ private val artistsRepository: ArtistRepository,
+ private val genresRepository: GenreRepository,
+ private val playlistsRepository: PlaylistRepository,
+ private val topPlayedRepository: TopPlayedRepository
+) {
+ private var mMusicService: WeakReference? = null
+
+ fun setMusicService(service: MusicService) {
+ mMusicService = WeakReference(service)
+ }
+
+ fun getChildren(mediaId: String?, resources: Resources): List {
+ val mediaItems: MutableList = ArrayList()
+ when (mediaId) {
+ AutoMediaIDHelper.MEDIA_ID_ROOT -> {
+ mediaItems.addAll(getRootChildren(resources))
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST -> for (playlist in playlistsRepository.playlists()) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST, playlist.id)
+ .icon(R.drawable.ic_playlist_play)
+ .title(playlist.name)
+ .subTitle(playlist.getInfoString(mContext))
+ .asPlayable()
+ .build()
+ )
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM -> for (album in albumsRepository.albums()) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .path(mediaId, album.id)
+ .title(album.title)
+ .subTitle(album.albumArtist ?: album.artistName)
+ .icon(MusicUtil.getMediaStoreAlbumCoverUri(album.id))
+ .asPlayable()
+ .build()
+ )
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST -> for (artist in artistsRepository.artists()) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asPlayable()
+ .path(mediaId, artist.id)
+ .title(artist.name)
+ .build()
+ )
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST -> for (artist in artistsRepository.albumArtists()) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asPlayable()
+ // we just pass album id here as we don't have album artist id's
+ .path(mediaId, artist.safeGetFirstAlbum().id)
+ .title(artist.name)
+ .build()
+ )
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE -> for (genre in genresRepository.genres()) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asPlayable()
+ .path(mediaId, genre.id)
+ .title(genre.name)
+ .build()
+ )
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE ->
+ mMusicService?.get()?.playingQueue
+ ?.let {
+ for (song in it) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asPlayable()
+ .path(mediaId, song.id)
+ .title(song.title)
+ .subTitle(song.artistName)
+ .icon(MusicUtil.getMediaStoreAlbumCoverUri(song.albumId))
+ .build()
+ )
+ }
+ }
+ else -> {
+ getPlaylistChildren(mediaId, mediaItems)
+ }
+ }
+ return mediaItems
+ }
+
+ private fun getPlaylistChildren(
+ mediaId: String?,
+ mediaItems: MutableList
+ ) {
+ val songs = when (mediaId) {
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS -> {
+ topPlayedRepository.topTracks()
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY -> {
+ topPlayedRepository.recentlyPlayedTracks()
+ }
+ AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS -> {
+ topPlayedRepository.notRecentlyPlayedTracks().take(8)
+ }
+ else -> {
+ emptyList()
+ }
+ }
+ songs.forEach { song ->
+ mediaItems.add(
+ getPlayableSong(mediaId, song)
+ )
+ }
+ }
+
+ private fun getRootChildren(resources: Resources): List {
+ val mediaItems: MutableList = ArrayList()
+ val libraryCategories = PreferenceUtil.libraryCategory
+ libraryCategories.forEach {
+ if (it.visible) {
+ when (it.category) {
+ CategoryInfo.Category.Albums -> {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM)
+ .gridLayout(true)
+ .icon(R.drawable.ic_album)
+ .title(resources.getString(R.string.albums)).build()
+ )
+ }
+ CategoryInfo.Category.Artists -> {
+ if (PreferenceUtil.albumArtistsOnly) {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST)
+ .icon(R.drawable.ic_album_artist)
+ .title(resources.getString(R.string.album_artist)).build()
+ )
+ } else {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST)
+ .icon(R.drawable.ic_artist)
+ .title(resources.getString(R.string.artists)).build()
+ )
+ }
+ }
+ CategoryInfo.Category.Genres -> {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE)
+ .icon(R.drawable.ic_guitar)
+ .title(resources.getString(R.string.genres)).build()
+ )
+ }
+ CategoryInfo.Category.Playlists -> {
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST)
+ .icon(R.drawable.ic_playlist_play)
+ .title(resources.getString(R.string.playlists)).build()
+ )
+ }
+ else -> {
+ }
+ }
+ }
+ }
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asPlayable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE)
+ .icon(R.drawable.ic_shuffle)
+ .title(resources.getString(R.string.action_shuffle_all))
+ .subTitle(MusicUtil.getPlaylistInfoString(mContext, songsRepository.songs()))
+ .build()
+ )
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE)
+ .icon(R.drawable.ic_queue_music)
+ .title(resources.getString(R.string.queue))
+ .subTitle(MusicUtil.getPlaylistInfoString(mContext, MusicPlayerRemote.playingQueue))
+ .asBrowsable().build()
+ )
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS)
+ .icon(R.drawable.ic_trending_up)
+ .title(resources.getString(R.string.my_top_tracks))
+ .subTitle(
+ MusicUtil.getPlaylistInfoString(
+ mContext,
+ topPlayedRepository.topTracks()
+ )
+ )
+ .asBrowsable().build()
+ )
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS)
+ .icon(R.drawable.ic_face)
+ .title(resources.getString(R.string.suggestion_songs))
+ .subTitle(
+ MusicUtil.getPlaylistInfoString(
+ mContext,
+ topPlayedRepository.notRecentlyPlayedTracks().takeIf {
+ it.size > 9
+ } ?: emptyList()
+ )
+ )
+ .asBrowsable().build()
+ )
+ mediaItems.add(
+ AutoMediaItem.with(mContext)
+ .asBrowsable()
+ .path(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY)
+ .icon(R.drawable.ic_history)
+ .title(resources.getString(R.string.history))
+ .subTitle(
+ MusicUtil.getPlaylistInfoString(
+ mContext,
+ topPlayedRepository.recentlyPlayedTracks()
+ )
+ )
+ .asBrowsable().build()
+ )
+ return mediaItems
+ }
+
+ private fun getPlayableSong(mediaId: String?, song: Song): MediaBrowserCompat.MediaItem {
+ return AutoMediaItem.with(mContext)
+ .asPlayable()
+ .path(mediaId, song.id)
+ .title(song.title)
+ .subTitle(song.artistName)
+ .icon(MusicUtil.getMediaStoreAlbumCoverUri(song.albumId))
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/auto/MediaItemBuilder.kt b/app/src/main/java/code/name/monkey/retromusic/auto/MediaItemBuilder.kt
new file mode 100644
index 000000000..534b0ec52
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/auto/MediaItemBuilder.kt
@@ -0,0 +1,100 @@
+package code.name.monkey.retromusic.auto
+
+import android.content.Context
+import android.net.Uri
+import android.support.v4.media.MediaBrowserCompat
+import android.support.v4.media.MediaDescriptionCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.os.bundleOf
+
+
+internal object AutoMediaItem {
+ fun with(context: Context): Builder {
+ return Builder(context)
+ }
+
+ internal class Builder(private val mContext: Context) {
+ private var mBuilder: MediaDescriptionCompat.Builder?
+ private var mFlags = 0
+ fun path(fullPath: String): Builder {
+ mBuilder?.setMediaId(fullPath)
+ return this
+ }
+
+ fun path(path: String?, id: Long): Builder {
+ return path(AutoMediaIDHelper.createMediaID(id.toString(), path))
+ }
+
+ fun title(title: String): Builder {
+ mBuilder?.setTitle(title)
+ return this
+ }
+
+ fun subTitle(subTitle: String): Builder {
+ mBuilder?.setSubtitle(subTitle)
+ return this
+ }
+
+ fun icon(uri: Uri?): Builder {
+ mBuilder?.setIconUri(uri)
+ return this
+ }
+
+ fun icon(iconDrawableId: Int): Builder {
+ mBuilder?.setIconBitmap(
+ ResourcesCompat.getDrawable(
+ mContext.resources,
+ iconDrawableId,
+ mContext.theme
+ )?.toBitmap()
+ )
+ return this
+ }
+
+ fun gridLayout(isGrid: Boolean): Builder {
+
+ val hints = bundleOf(
+ CONTENT_STYLE_SUPPORTED to true,
+ CONTENT_STYLE_BROWSABLE_HINT to
+ if (isGrid) CONTENT_STYLE_GRID_ITEM_HINT_VALUE
+ else CONTENT_STYLE_LIST_ITEM_HINT_VALUE,
+ CONTENT_STYLE_PLAYABLE_HINT to
+ if (isGrid) CONTENT_STYLE_GRID_ITEM_HINT_VALUE
+ else CONTENT_STYLE_LIST_ITEM_HINT_VALUE
+ )
+ mBuilder?.setExtras(hints)
+ return this
+ }
+
+ fun asBrowsable(): Builder {
+ mFlags = mFlags or MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
+ return this
+ }
+
+ fun asPlayable(): Builder {
+ mFlags = mFlags or MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
+ return this
+ }
+
+ fun build(): MediaBrowserCompat.MediaItem {
+ val result = MediaBrowserCompat.MediaItem(mBuilder!!.build(), mFlags)
+ mBuilder = null
+ mFlags = 0
+ return result
+ }
+
+ init {
+ mBuilder = MediaDescriptionCompat.Builder()
+ }
+ companion object{
+ // Hints - see https://developer.android.com/training/cars/media#default-content-style
+ const val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"
+ const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"
+ const val CONTENT_STYLE_PLAYABLE_HINT = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"
+ const val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1
+ const val CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/CastHelper.java b/app/src/main/java/code/name/monkey/retromusic/cast/CastHelper.java
deleted file mode 100644
index a8951ee01..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/cast/CastHelper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package code.name.monkey.retromusic.cast;
-
-import android.net.Uri;
-
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaLoadOptions;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.cast.framework.CastSession;
-import com.google.android.gms.cast.framework.media.RemoteMediaClient;
-import com.google.android.gms.common.images.WebImage;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import code.name.monkey.retromusic.Constants;
-import code.name.monkey.retromusic.model.Song;
-import code.name.monkey.retromusic.util.RetroUtil;
-
-import static code.name.monkey.retromusic.Constants.CAST_SERVER_PORT;
-
-public class CastHelper {
-
- public static void startCasting(CastSession castSession, Song song) {
-
- String ipAddress = RetroUtil.getIPAddress(true);
- URL baseUrl;
- try {
- baseUrl = new URL("https", ipAddress,CAST_SERVER_PORT, "");
- } catch (MalformedURLException e) {
- e.printStackTrace();
- return;
- }
-
- String songUrl = baseUrl.toString() + "/song?id=" + song.getId();
- String albumArtUrl = baseUrl.toString() + "/albumart?id=" + song.getAlbumId();
-
-
- MediaMetadata musicMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
-
- musicMetadata.putString(MediaMetadata.KEY_TITLE, song.getTitle());
- musicMetadata.putString(MediaMetadata.KEY_ARTIST, song.getArtistName());
- musicMetadata.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbumName());
- musicMetadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrackNumber());
- musicMetadata.addImage(new WebImage(Uri.parse(albumArtUrl)));
-
- try {
- MediaInfo mediaInfo = new MediaInfo.Builder(songUrl)
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setContentType("audio/mpeg")
- .setMetadata(musicMetadata)
- .setStreamDuration(song.getDuration())
- .build();
- RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient();
- remoteMediaClient.load(mediaInfo, new MediaLoadOptions.Builder()
- .build());
- //remoteMediaClient.load(mediaInfo, true, 0);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-}
diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/CastOptionsProvider.java b/app/src/main/java/code/name/monkey/retromusic/cast/CastOptionsProvider.java
deleted file mode 100644
index 9002bcf31..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/cast/CastOptionsProvider.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package code.name.monkey.retromusic.cast;
-
-import android.content.Context;
-
-import com.google.android.gms.cast.framework.CastOptions;
-import com.google.android.gms.cast.framework.OptionsProvider;
-import com.google.android.gms.cast.framework.SessionProvider;
-import com.google.android.gms.cast.framework.media.CastMediaOptions;
-import com.google.android.gms.cast.framework.media.MediaIntentReceiver;
-import com.google.android.gms.cast.framework.media.NotificationOptions;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import code.name.monkey.retromusic.R;
-
-public class CastOptionsProvider implements OptionsProvider {
- @Override
- public CastOptions getCastOptions(Context context) {
- List buttonActions = new ArrayList<>();
- buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK);
- buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING);
- int[] compatButtonActionsIndicies = new int[]{ 0, 1 };
-
- NotificationOptions notificationOptions = new NotificationOptions.Builder()
- .setActions(buttonActions, compatButtonActionsIndicies)
- .setTargetActivityClassName(ExpandedCastControlsActivity.class.getName())
- .build();
-
- CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
- .setNotificationOptions(notificationOptions)
- .setExpandedControllerActivityClassName(ExpandedCastControlsActivity.class.getName())
- .build();
-
- return new CastOptions.Builder()
- .setReceiverApplicationId(context.getString(R.string.cast_app_id))
- .setCastMediaOptions(mediaOptions)
- .build();
- }
-
- @Override
- public List getAdditionalSessionProviders(Context context) {
- return null;
- }
-}
diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/ExpandedCastControlsActivity.java b/app/src/main/java/code/name/monkey/retromusic/cast/ExpandedCastControlsActivity.java
deleted file mode 100644
index b003314f7..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/cast/ExpandedCastControlsActivity.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package code.name.monkey.retromusic.cast;
-
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.View;
-
-import com.google.android.gms.cast.framework.CastButtonFactory;
-import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity;
-
-import code.name.monkey.retromusic.R;
-
-
-public class ExpandedCastControlsActivity extends ExpandedControllerActivity {
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- getMenuInflater().inflate(R.menu.menu_expanded_controller, menu);
- CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
- return true;
- }
-
- @Override
- protected void onCreate(Bundle bundle) {
- super.onCreate(bundle);
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/cast/WebServer.java b/app/src/main/java/code/name/monkey/retromusic/cast/WebServer.java
deleted file mode 100644
index 172514de9..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/cast/WebServer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package code.name.monkey.retromusic.cast;
-
-import android.content.Context;
-import android.net.Uri;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.util.Map;
-
-import code.name.monkey.retromusic.Constants;
-import code.name.monkey.retromusic.util.RetroUtil;
-import fi.iki.elonen.NanoHTTPD;
-
-import static code.name.monkey.retromusic.Constants.CAST_SERVER_PORT;
-
-public class WebServer extends NanoHTTPD {
-
- private Context context;
- private Uri songUri, albumArtUri;
-
- public WebServer(Context context) {
- super(CAST_SERVER_PORT);
- this.context = context;
- }
-
- @Override
- public Response serve(String uri, Method method,
- Map header,
- Map parameters,
- Map files) {
- if (uri.contains("albumart")) {
- //serve the picture
-
- String albumId = parameters.get("id");
- this.albumArtUri = RetroUtil.getAlbumArtUri(Long.parseLong(albumId));
-
- if (albumArtUri != null) {
- String mediasend = "image/jpg";
- InputStream fisAlbumArt = null;
- try {
- fisAlbumArt = context.getContentResolver().openInputStream(albumArtUri);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
- Response.Status st = Response.Status.OK;
-
- //serve the song
- return newChunkedResponse(st, mediasend, fisAlbumArt);
- }
-
- } else if (uri.contains("song")) {
-
- String songId = parameters.get("id");
- this.songUri = RetroUtil.getSongUri(context, Long.parseLong(songId));
-
- if (songUri != null) {
- String mediasend = "audio/mp3";
- FileInputStream fisSong = null;
- File song = new File(songUri.getPath());
- try {
- fisSong = new FileInputStream(song);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
- Response.Status st = Response.Status.OK;
-
- //serve the song
- return newFixedLengthResponse(st, mediasend, fisSong, song.length());
- }
-
- }
- return newFixedLengthResponse("Error");
- }
-
-}
\ No newline at end of file
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
new file mode 100644
index 000000000..0bf40b736
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreDao.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.*
+
+@Dao
+interface BlackListStoreDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertBlacklistPath(blackListStoreEntity: BlackListStoreEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertBlacklistPath(blackListStoreEntities: List)
+
+ @Delete
+ suspend fun deleteBlacklistPath(blackListStoreEntity: BlackListStoreEntity)
+
+ @Query("DELETE FROM BlackListStoreEntity")
+ suspend fun clearBlacklist()
+
+ @Query("SELECT * FROM BlackListStoreEntity")
+ fun blackListPaths(): List
+}
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
new file mode 100644
index 000000000..8592442a2
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/BlackListStoreEntity.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.PrimaryKey
+
+@Entity
+class BlackListStoreEntity(
+ @PrimaryKey
+ val path: String
+)
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
new file mode 100644
index 000000000..2ceb309db
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/HistoryDao.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.*
+
+@Dao
+interface HistoryDao {
+ companion object {
+ private const val HISTORY_LIMIT = 100
+ }
+
+ @Upsert
+ suspend fun upsertSongInHistory(historyEntity: HistoryEntity)
+
+ @Query("DELETE FROM HistoryEntity WHERE id= :songId")
+ fun deleteSongInHistory(songId: Long)
+
+ @Query("SELECT * FROM HistoryEntity ORDER BY time_played DESC LIMIT $HISTORY_LIMIT")
+ fun historySongs(): List
+
+ @Query("SELECT * FROM HistoryEntity ORDER BY time_played DESC LIMIT $HISTORY_LIMIT")
+ fun observableHistorySongs(): LiveData>
+
+ @Query("DELETE FROM HistoryEntity")
+ suspend fun clearHistory()
+}
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
new file mode 100644
index 000000000..535a37964
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/HistoryEntity.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+class HistoryEntity(
+ @PrimaryKey
+ val id: Long,
+ val title: String,
+ @ColumnInfo(name = "track_number")
+ val trackNumber: Int,
+ val year: Int,
+ val duration: Long,
+ val data: String,
+ @ColumnInfo(name = "date_modified")
+ val dateModified: Long,
+ @ColumnInfo(name = "album_id")
+ val albumId: Long,
+ @ColumnInfo(name = "album_name")
+ val albumName: String,
+ @ColumnInfo(name = "artist_id")
+ val artistId: Long,
+ @ColumnInfo(name = "artist_name")
+ val artistName: String,
+ val composer: String?,
+ @ColumnInfo(name = "album_artist")
+ val albumArtist: String?,
+ @ColumnInfo(name = "time_played")
+ val timePlayed: Long
+)
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
new file mode 100644
index 000000000..fa14b1aca
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/LyricsDao.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.*
+
+@Dao
+interface LyricsDao {
+ @Query("SELECT * FROM LyricsEntity WHERE songId =:songId LIMIT 1")
+ fun lyricsWithSongId(songId: Int): LyricsEntity?
+
+ @Insert
+ fun insertLyrics(lyricsEntity: LyricsEntity)
+
+ @Delete
+ fun deleteLyrics(lyricsEntity: LyricsEntity)
+
+ @Update
+ fun updateLyrics(lyricsEntity: LyricsEntity)
+}
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
new file mode 100644
index 000000000..91d987d62
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/LyricsEntity.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.PrimaryKey
+
+@Entity
+class LyricsEntity(
+ @PrimaryKey val songId: Int,
+ val lyrics: String
+)
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
new file mode 100644
index 000000000..087ae4faf
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountDao.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.*
+
+@Dao
+interface PlayCountDao {
+
+ @Upsert
+ fun upsertSongInPlayCount(playCountEntity: PlayCountEntity)
+
+ @Delete
+ fun deleteSongInPlayCount(playCountEntity: PlayCountEntity)
+
+ @Query("SELECT * FROM PlayCountEntity WHERE id =:songId LIMIT 1")
+ fun findSongExistInPlayCount(songId: Long): PlayCountEntity?
+
+ @Query("SELECT * FROM PlayCountEntity ORDER BY play_count DESC")
+ fun playCountSongs(): List
+
+ @Query("DELETE FROM SongEntity WHERE id =:songId")
+ fun deleteSong(songId: Long)
+}
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
new file mode 100644
index 000000000..2fa41b227
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/PlayCountEntity.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+class PlayCountEntity(
+ @PrimaryKey
+ val id: Long,
+ val title: String,
+ @ColumnInfo(name = "track_number")
+ val trackNumber: Int,
+ val year: Int,
+ val duration: Long,
+ val data: String,
+ @ColumnInfo(name = "date_modified")
+ val dateModified: Long,
+ @ColumnInfo(name = "album_id")
+ val albumId: Long,
+ @ColumnInfo(name = "album_name")
+ val albumName: String,
+ @ColumnInfo(name = "artist_id")
+ val artistId: Long,
+ @ColumnInfo(name = "artist_name")
+ val artistName: String,
+ val composer: String?,
+ @ColumnInfo(name = "album_artist")
+ val albumArtist: String?,
+ @ColumnInfo(name = "time_played")
+ val timePlayed: Long,
+ @ColumnInfo(name = "play_count")
+ var playCount: Int
+)
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
new file mode 100644
index 000000000..dea1e6b7d
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistDao.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.*
+
+@Dao
+interface PlaylistDao {
+ @Insert
+ suspend fun createPlaylist(playlistEntity: PlaylistEntity): Long
+
+ @Query("UPDATE PlaylistEntity SET playlist_name = :name WHERE playlist_id = :playlistId")
+ suspend fun renamePlaylist(playlistId: Long, name: String)
+
+ @Query("SELECT * FROM PlaylistEntity WHERE playlist_name = :name")
+ fun playlist(name: String): List
+
+ @Query("SELECT * FROM PlaylistEntity")
+ suspend fun playlists(): List
+
+ @Query("DELETE FROM SongEntity WHERE playlist_creator_id = :playlistId")
+ suspend fun deletePlaylistSongs(playlistId: Long)
+
+ @Query("DELETE FROM SongEntity WHERE playlist_creator_id = :playlistId AND id = :songId")
+ suspend fun deleteSongFromPlaylist(playlistId: Long, songId: Long)
+
+ @Transaction
+ @Query("SELECT * FROM PlaylistEntity")
+ suspend fun playlistsWithSongs(): List
+
+ @Transaction
+ @Query("SELECT * FROM PlaylistEntity WHERE playlist_id= :playlistId")
+ fun getPlaylist(playlistId: Long): LiveData
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertSongsToPlaylist(songEntities: List)
+
+ @Query("SELECT * FROM SongEntity WHERE playlist_creator_id = :playlistId AND id = :songId")
+ suspend fun isSongExistsInPlaylist(playlistId: Long, songId: Long): List
+
+ @Query("SELECT * FROM SongEntity WHERE playlist_creator_id = :playlistId ORDER BY song_key asc")
+ fun songsFromPlaylist(playlistId: Long): LiveData>
+
+ @Delete
+ suspend fun deletePlaylist(playlistEntity: PlaylistEntity)
+
+ @Delete
+ suspend fun deletePlaylists(playlistEntities: List)
+
+ @Delete
+ suspend fun deletePlaylistSongs(songs: List)
+
+ @RewriteQueriesToDropUnusedColumns
+ @Query("SELECT * FROM SongEntity ,(SELECT playlist_id FROM PlaylistEntity WHERE playlist_name= :playlistName LIMIT 1) AS playlist WHERE playlist_creator_id= playlist.playlist_id")
+ fun favoritesSongsLiveData(playlistName: String): LiveData>
+
+ @Query("SELECT * FROM SongEntity WHERE playlist_creator_id= :playlistId")
+ fun favoritesSongs(playlistId: Long): List
+
+ @Query("SELECT EXISTS(SELECT * FROM PlaylistEntity WHERE playlist_id = :playlistId)")
+ fun checkPlaylistExists(playlistId: Long): LiveData
+}
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
new file mode 100644
index 000000000..5493b1155
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistEntity.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import kotlinx.parcelize.Parcelize
+
+@Entity
+@Parcelize
+class PlaylistEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "playlist_id")
+ val playListId: Long = 0,
+ @ColumnInfo(name = "playlist_name")
+ val playlistName: String
+) : 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
new file mode 100644
index 000000000..da80d8228
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/PlaylistWithSongs.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.Embedded
+import androidx.room.Relation
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class PlaylistWithSongs(
+ @Embedded val playlistEntity: PlaylistEntity,
+ @Relation(
+ parentColumn = "playlist_id",
+ entityColumn = "playlist_creator_id"
+ )
+ 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
new file mode 100644
index 000000000..6401766aa
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/RetroDatabase.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.RoomDatabase
+
+@Database(
+ entities = [PlaylistEntity::class, SongEntity::class, HistoryEntity::class, PlayCountEntity::class],
+ version = 24,
+ exportSchema = false
+)
+abstract class RetroDatabase : RoomDatabase() {
+ abstract fun playlistDao(): PlaylistDao
+ abstract fun playCountDao(): PlayCountDao
+ abstract fun historyDao(): HistoryDao
+}
diff --git a/app/src/main/java/code/name/monkey/retromusic/db/RoomMigrations.kt b/app/src/main/java/code/name/monkey/retromusic/db/RoomMigrations.kt
new file mode 100644
index 000000000..b8b383c31
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/RoomMigrations.kt
@@ -0,0 +1,11 @@
+package code.name.monkey.retromusic.db
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+val MIGRATION_23_24 = object : Migration(23, 24) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("DROP TABLE LyricsEntity")
+ database.execSQL("DROP TABLE BlackListStoreEntity")
+ }
+}
\ 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
new file mode 100644
index 000000000..206c91e27
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/SongEntity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@Entity(indices = [Index(value = ["playlist_creator_id", "id"], unique = true)])
+class SongEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "song_key")
+ val songPrimaryKey: Long = 0L,
+ @ColumnInfo(name = "playlist_creator_id")
+ val playlistCreatorId: Long,
+ val id: Long,
+ val title: String,
+ @ColumnInfo(name = "track_number")
+ val trackNumber: Int,
+ val year: Int,
+ val duration: Long,
+ val data: String,
+ @ColumnInfo(name = "date_modified")
+ val dateModified: Long,
+ @ColumnInfo(name = "album_id")
+ val albumId: Long,
+ @ColumnInfo(name = "album_name")
+ val albumName: String,
+ @ColumnInfo(name = "artist_id")
+ val artistId: Long,
+ @ColumnInfo(name = "artist_name")
+ val artistName: String,
+ val composer: String?,
+ @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
new file mode 100644
index 000000000..d6fe11b05
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/db/SongExtension.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
+
+fun List.fromHistoryToSongs(): List {
+ return map {
+ it.toSong()
+ }
+}
+
+fun List.toSongs(): List {
+ return map {
+ it.toSong()
+ }
+}
+
+fun Song.toHistoryEntity(timePlayed: Long): HistoryEntity {
+ return HistoryEntity(
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist,
+ timePlayed = timePlayed
+ )
+}
+
+fun Song.toSongEntity(playListId: Long): SongEntity {
+ return SongEntity(
+ playlistCreatorId = playListId,
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist
+ )
+}
+
+fun SongEntity.toSong(): Song {
+ return Song(
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist
+ )
+}
+
+fun PlayCountEntity.toSong(): Song {
+ return Song(
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist
+ )
+}
+
+fun HistoryEntity.toSong(): Song {
+ return Song(
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist
+ )
+}
+
+fun Song.toPlayCount(): PlayCountEntity {
+ return PlayCountEntity(
+ id = id,
+ title = title,
+ trackNumber = trackNumber,
+ year = year,
+ duration = duration,
+ data = data,
+ dateModified = dateModified,
+ albumId = albumId,
+ albumName = albumName,
+ artistId = artistId,
+ artistName = artistName,
+ composer = composer,
+ albumArtist = albumArtist,
+ timePlayed = System.currentTimeMillis(),
+ playCount = 1
+ )
+}
+
+fun List.toSongsEntity(playlistEntity: PlaylistEntity): List {
+ return map {
+ it.toSongEntity(playlistEntity.playListId)
+ }
+}
+
+fun List.toSongsEntity(playlistId: Long): List {
+ return map {
+ it.toSongEntity(playlistId)
+ }
+}
\ No newline at end of file
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 5c7b8481d..aa47f0fe2 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,66 +1,77 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DefaultItemAnimator
-import androidx.recyclerview.widget.LinearLayoutManager
-import code.name.monkey.appthemehelper.ThemeStore
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import code.name.monkey.retromusic.EXTRA_PLAYLISTS
+import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
-import code.name.monkey.retromusic.loaders.PlaylistLoader
+import code.name.monkey.retromusic.db.PlaylistEntity
+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.model.Song
-import code.name.monkey.retromusic.ui.adapter.playlist.AddToPlaylist
-import code.name.monkey.retromusic.views.RoundedBottomSheetDialogFragment
-import kotlinx.android.synthetic.main.dialog_add_to_playlist.*
-import java.util.*
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
-/**
- * @author Karim Abou Zeid (kabouzeid), Aidan Follestad (afollestad)
- */
-class AddToPlaylistDialog : RoundedBottomSheetDialogFragment() {
-
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
-
- return inflater.inflate(R.layout.dialog_add_to_playlist, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- actionAddPlaylist.setOnClickListener {
- val songs = arguments!!.getParcelableArrayList("songs")
- CreatePlaylistDialog.create(songs).show(activity!!.supportFragmentManager, "ADD_TO_PLAYLIST")
- dismiss()
- }
-
- bannerTitle.setTextColor(ThemeStore.textColorPrimary(context!!))
- val songs = arguments!!.getParcelableArrayList("songs")
- val playlists = PlaylistLoader.getAllPlaylists(activity!!).blockingFirst()
- val playlistAdapter = AddToPlaylist(activity!!, playlists, R.layout.item_playlist, songs!!, dialog)
- recyclerView.apply {
- layoutManager = LinearLayoutManager(context)
- itemAnimator = DefaultItemAnimator()
- adapter = playlistAdapter
- }
- }
+class AddToPlaylistDialog : DialogFragment() {
+ private val libraryViewModel by activityViewModel()
companion object {
-
- fun create(song: Song): AddToPlaylistDialog {
- val list = ArrayList()
+ fun create(playlistEntities: List, song: Song): AddToPlaylistDialog {
+ val list: MutableList = mutableListOf()
list.add(song)
- return create(list)
+ return create(playlistEntities, list)
}
- fun create(songs: ArrayList): AddToPlaylistDialog {
- val dialog = AddToPlaylistDialog()
- val args = Bundle()
- args.putParcelableArrayList("songs", songs)
- dialog.arguments = args
- return dialog
+ fun create(playlistEntities: List, songs: List): AddToPlaylistDialog {
+ return AddToPlaylistDialog().apply {
+ arguments = bundleOf(
+ EXTRA_SONG to songs,
+ EXTRA_PLAYLISTS to playlistEntities
+ )
+ }
}
}
-}
\ No newline at end of file
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val playlistEntities = extraNotNull>(EXTRA_PLAYLISTS).value
+ val songs = extraNotNull>(EXTRA_SONG).value
+ val playlistNames = mutableListOf()
+ playlistNames.add(requireContext().resources.getString(R.string.action_new_playlist))
+ for (entity: PlaylistEntity in playlistEntities) {
+ playlistNames.add(entity.playlistName)
+ }
+ return materialDialog(R.string.add_playlist_title)
+ .setItems(playlistNames.toTypedArray()) { dialog, which ->
+ if (which == 0) {
+ showCreateDialog(songs)
+ } else {
+ libraryViewModel.addToPlaylist(requireContext(), playlistNames[which], songs)
+ }
+ dialog.dismiss()
+ }
+ .setNegativeButton(R.string.action_cancel, null)
+ .create()
+ .colorButtons()
+ }
+
+ private fun showCreateDialog(songs: List) {
+ CreatePlaylistDialog.create(songs).show(requireActivity().supportFragmentManager, "Dialog")
+ }
+}
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
deleted file mode 100644
index 0bd45f818..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package code.name.monkey.retromusic.dialogs;
-
-import android.Manifest;
-import android.app.Dialog;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.view.View;
-
-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 androidx.annotation.NonNull;
-import androidx.core.app.ActivityCompat;
-import androidx.fragment.app.DialogFragment;
-import code.name.monkey.retromusic.R;
-
-/**
- * @author Aidan Follestad (afollestad), modified by Karim Abou Zeid
- */
-public class BlacklistFolderChooserDialog extends DialogFragment implements MaterialDialog.ListCallback {
-
- private 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();
- }
-
- 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;
- }
-
- 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(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- return new MaterialDialog.Builder(getActivity())
- .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(getActivity())
- .title(parentFolder.getAbsolutePath())
- .items((CharSequence[]) getContentsArray())
- .itemsCallback(this)
- .autoDismiss(false)
- .onPositive((dialog, which) -> {
- dismiss();
- callback.onFolderSelection(BlacklistFolderChooserDialog.this, parentFolder);
- })
- .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 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/BlacklistFolderChooserDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.kt
new file mode 100644
index 000000000..5701d858b
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/BlacklistFolderChooserDialog.kt
@@ -0,0 +1,140 @@
+package code.name.monkey.retromusic.dialogs
+
+import android.Manifest
+import android.app.Dialog
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.core.app.ActivityCompat
+import androidx.fragment.app.DialogFragment
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.extensions.materialDialog
+import code.name.monkey.retromusic.util.getExternalStorageDirectory
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.list.listItems
+import com.afollestad.materialdialogs.list.updateListItems
+import java.io.File
+
+class BlacklistFolderChooserDialog : DialogFragment() {
+ private var initialPath: String = getExternalStorageDirectory().absolutePath
+ private var parentFolder: File? = null
+ private var parentContents: Array? = null
+ private var canGoUp = false
+ private var callback: FolderCallback? = null
+ private val contentsArray: Array
+ get() {
+ if (parentContents == null) {
+ return if (canGoUp) {
+ arrayOf("..")
+ } else arrayOf()
+ }
+ val results = arrayOfNulls(parentContents!!.size + if (canGoUp) 1 else 0)
+ if (canGoUp) {
+ results[0] = ".."
+ }
+ for (i in parentContents!!.indices) {
+ results[if (canGoUp) i + 1 else i] = parentContents?.getOrNull(i)?.name
+ }
+ return results
+ }
+
+ private fun listFiles(): Array? {
+ val results = mutableListOf()
+ parentFolder?.listFiles()?.let { files ->
+ files.forEach { file -> if (file.isDirectory) results.add(file) }
+ return results.sortedBy { it.name }.toTypedArray()
+ }
+ return null
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ var mSavedInstanceState = savedInstanceState
+ if (VersionUtils.hasMarshmallow()
+ && ActivityCompat.checkSelfPermission(
+ requireActivity(), Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ return materialDialog().show {
+ title(res = R.string.md_error_label)
+ message(res = R.string.md_storage_perm_error)
+ positiveButton(res = android.R.string.ok)
+ }
+ }
+ if (mSavedInstanceState == null) {
+ mSavedInstanceState = Bundle()
+ }
+ if (!mSavedInstanceState.containsKey("current_path")) {
+ mSavedInstanceState.putString("current_path", initialPath)
+ }
+ parentFolder = File(mSavedInstanceState.getString("current_path", File.pathSeparator))
+ checkIfCanGoUp()
+ parentContents = listFiles()
+ return materialDialog()
+ .title(text = parentFolder?.absolutePath)
+ .listItems(
+ items = contentsArray.toCharSequence(),
+ waitForPositiveButton = false
+ ) { _: MaterialDialog, i: Int, _: CharSequence ->
+ onSelection(i)
+ }
+ .noAutoDismiss()
+ .positiveButton(res = R.string.add_action) {
+ callback?.onFolderSelection(requireContext(), parentFolder!!)
+ dismiss()
+ }
+ .negativeButton(res = android.R.string.cancel) { dismiss() }
+ }
+
+ private fun onSelection(i: Int) {
+ if (canGoUp && i == 0) {
+ parentFolder = parentFolder?.parentFile
+ if (parentFolder?.absolutePath == "/storage/emulated") {
+ parentFolder = parentFolder?.parentFile
+ }
+ checkIfCanGoUp()
+ } else {
+ parentFolder = parentContents?.getOrNull(if (canGoUp) i - 1 else i)
+ canGoUp = true
+ if (parentFolder?.absolutePath == "/storage/emulated") {
+ parentFolder = getExternalStorageDirectory()
+ }
+ }
+ reload()
+ }
+
+ private fun checkIfCanGoUp() {
+ canGoUp = parentFolder?.parent != null
+ }
+
+ private fun reload() {
+ parentContents = listFiles()
+ val dialog = dialog as MaterialDialog?
+ dialog?.setTitle(parentFolder?.absolutePath)
+ dialog?.updateListItems(items = contentsArray.toCharSequence())
+ }
+
+ private fun Array.toCharSequence(): List {
+ return map { it as CharSequence }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString("current_path", parentFolder?.absolutePath)
+ }
+
+ fun setCallback(callback: FolderCallback?) {
+ this.callback = callback
+ }
+
+ interface FolderCallback {
+ fun onFolderSelection(context: Context, folder: File)
+ }
+
+ companion object {
+ fun create(): BlacklistFolderChooserDialog {
+ return BlacklistFolderChooserDialog()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/ClearSmartPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/ClearSmartPlaylistDialog.kt
deleted file mode 100644
index 500d95dce..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/dialogs/ClearSmartPlaylistDialog.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package code.name.monkey.retromusic.dialogs
-
-import android.app.Dialog
-import android.os.Bundle
-import android.text.Html
-import androidx.fragment.app.DialogFragment
-import code.name.monkey.retromusic.R
-import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist
-import com.afollestad.materialdialogs.MaterialDialog
-
-
-class ClearSmartPlaylistDialog : DialogFragment() {
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-
- val playlist = arguments!!.getParcelable("playlist")
- val title = R.string.clear_playlist_title
-
- val content = Html.fromHtml(getString(R.string.clear_playlist_x, playlist!!.name))
-
- return MaterialDialog.Builder(activity!!)
- .title(title)
- .content(content)
- .positiveText(R.string.clear_action)
- .negativeText(android.R.string.cancel)
- .onPositive { _, _ ->
- if (activity == null) {
- return@onPositive
- }
- playlist.clear(activity)
- }
- .build()
- }
-
- companion object {
-
- fun create(playlist: AbsSmartPlaylist): ClearSmartPlaylistDialog {
- val dialog = ClearSmartPlaylistDialog()
- val args = Bundle()
- args.putParcelable("playlist", playlist)
- dialog.arguments = args
- return dialog
- }
- }
-}
\ 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 73982455e..7d7a01c90 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,81 +1,81 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.content.Context
-import android.content.res.ColorStateList
+import android.app.Dialog
import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import code.name.monkey.appthemehelper.ThemeStore
-import code.name.monkey.appthemehelper.util.MaterialUtil
+import android.text.TextUtils
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
+import code.name.monkey.retromusic.databinding.DialogPlaylistBinding
+import code.name.monkey.retromusic.extensions.colorButtons
+import code.name.monkey.retromusic.extensions.extra
+import code.name.monkey.retromusic.extensions.materialDialog
+import code.name.monkey.retromusic.fragments.LibraryViewModel
import code.name.monkey.retromusic.model.Song
-import code.name.monkey.retromusic.util.PlaylistsUtil
-import code.name.monkey.retromusic.views.RoundedBottomSheetDialogFragment
-import kotlinx.android.synthetic.main.dialog_playlist.*
-import java.util.*
+import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
-/**
- * @author Karim Abou Zeid (kabouzeid), Aidan Follestad (afollestad)
- */
-class CreatePlaylistDialog : RoundedBottomSheetDialogFragment() {
-
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
-
- return inflater.inflate(R.layout.dialog_playlist, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val accentColor = ThemeStore.accentColor(Objects.requireNonNull(context))
-
- MaterialUtil.setTint(actionCreate, true)
- MaterialUtil.setTint(actionCancel, false)
- MaterialUtil.setTint(actionNewPlaylistContainer, true)
-
- actionNewPlaylist.setHintTextColor(ColorStateList.valueOf(accentColor))
- actionNewPlaylist.setTextColor(ThemeStore.textColorPrimary(context!!))
- bannerTitle.setTextColor(ThemeStore.textColorPrimary(context!!))
-
-
- actionCancel.setOnClickListener { dismiss() }
- actionCreate.setOnClickListener {
- if (activity == null) {
- return@setOnClickListener
- }
- if (!actionNewPlaylist!!.text!!.toString().trim { it <= ' ' }.isEmpty()) {
- val playlistId = PlaylistsUtil
- .createPlaylist(activity!!, actionNewPlaylist!!.text!!.toString())
- if (playlistId != -1 && activity != null) {
-
- val songs = arguments!!.getParcelableArrayList("songs")
- if (songs != null) {
- PlaylistsUtil.addToPlaylist(activity!!, songs, playlistId, true)
- }
- }
- }
- dismiss()
- }
- }
+class CreatePlaylistDialog : DialogFragment() {
+ private var _binding: DialogPlaylistBinding? = null
+ private val binding get() = _binding!!
+ private val libraryViewModel by activityViewModel()
companion object {
-
- @JvmOverloads
- fun create(song: Song? = null): CreatePlaylistDialog {
- val list = ArrayList()
- if (song != null) {
- list.add(song)
- }
+ fun create(song: Song): CreatePlaylistDialog {
+ val list = mutableListOf()
+ list.add(song)
return create(list)
}
- fun create(songs: ArrayList): CreatePlaylistDialog {
- val dialog = CreatePlaylistDialog()
- val args = Bundle()
- args.putParcelableArrayList("songs", songs)
- dialog.arguments = args
- return dialog
+ fun create(songs: List): CreatePlaylistDialog {
+ return CreatePlaylistDialog().apply {
+ arguments = bundleOf(EXTRA_SONG to songs)
+ }
}
}
-}
\ No newline at end of file
+
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ _binding = DialogPlaylistBinding.inflate(layoutInflater)
+
+ val songs: List = extra>(EXTRA_SONG).value ?: emptyList()
+ val playlistView: TextInputEditText = binding.actionNewPlaylist
+ val playlistContainer: TextInputLayout = binding.actionNewPlaylistContainer
+ return materialDialog(R.string.new_playlist_title)
+ .setView(binding.root)
+ .setPositiveButton(
+ R.string.create_action
+ ) { _, _ ->
+ val playlistName = playlistView.text.toString()
+ if (!TextUtils.isEmpty(playlistName)) {
+ libraryViewModel.addToPlaylist(requireContext(), playlistName, songs)
+ } else {
+ playlistContainer.error = "Playlist name can't be empty"
+ }
+ }
+ .setNegativeButton(R.string.action_cancel, null)
+ .create()
+ .colorButtons()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
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 f2b91e059..30675d88a 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,67 +1,79 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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
import android.os.Bundle
-import android.text.Html
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import code.name.monkey.appthemehelper.ThemeStore
+import androidx.core.os.bundleOf
+import androidx.core.text.parseAsHtml
+import androidx.fragment.app.DialogFragment
+import code.name.monkey.retromusic.EXTRA_PLAYLIST
import code.name.monkey.retromusic.R
-import code.name.monkey.retromusic.model.Playlist
-import code.name.monkey.retromusic.util.PlaylistsUtil
-import code.name.monkey.retromusic.views.RoundedBottomSheetDialogFragment
-import kotlinx.android.synthetic.main.dialog_remove_from_playlist.*
-import java.util.*
+import code.name.monkey.retromusic.db.PlaylistEntity
+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
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
+class DeletePlaylistDialog : DialogFragment() {
-class DeletePlaylistDialog : RoundedBottomSheetDialogFragment() {
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
- return inflater.inflate(R.layout.dialog_remove_from_playlist, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val playlists = arguments!!.getParcelableArrayList("playlists")
- val content: CharSequence
-
- content = if (playlists!!.size > 1) {
- Html.fromHtml(getString(R.string.delete_x_playlists, playlists.size))
- } else {
- Html.fromHtml(getString(R.string.delete_playlist_x, playlists[0].name))
- }
- bannerTitle.text = content
- bannerTitle.setTextColor(ThemeStore.textColorPrimary(context!!))
-
- actionRemove.setText(R.string.action_delete)
- actionRemove.setTextColor(ThemeStore.textColorSecondary(context!!))
- actionCancel.setTextColor(ThemeStore.textColorSecondary(context!!))
-
- actionCancel.setOnClickListener { dismiss() }
- actionRemove.setOnClickListener {
- PlaylistsUtil.deletePlaylists(activity!!, playlists)
- dismiss()
- }
- }
-
+ private val libraryViewModel by activityViewModel()
companion object {
- fun create(playlist: Playlist): DeletePlaylistDialog {
- val list = ArrayList()
+ fun create(playlist: PlaylistEntity): DeletePlaylistDialog {
+ val list = mutableListOf()
list.add(playlist)
return create(list)
}
- fun create(playlist: ArrayList): DeletePlaylistDialog {
- val dialog = DeletePlaylistDialog()
- val args = Bundle()
- args.putParcelableArrayList("playlist", playlist)
- dialog.arguments = args
- return dialog
+ fun create(playlists: List): DeletePlaylistDialog {
+ return DeletePlaylistDialog().apply {
+ arguments = bundleOf(EXTRA_PLAYLIST to playlists)
+ }
}
}
-}
\ No newline at end of file
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val playlists = extraNotNull>(EXTRA_PLAYLIST).value
+ val title: Int
+ val message: CharSequence
+ //noinspection ConstantConditions
+ if (playlists.size > 1) {
+ title = R.string.delete_playlists_title
+ message =
+ String.format(getString(R.string.delete_x_playlists), playlists.size).parseAsHtml()
+ } else {
+ title = R.string.delete_playlist_title
+ message =
+ String.format(getString(R.string.delete_playlist_x), playlists[0].playlistName)
+ .parseAsHtml()
+ }
+
+ return materialDialog(title)
+ .setTitle(title)
+ .setMessage(message)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.action_delete) { _, _ ->
+ libraryViewModel.deleteSongsFromPlaylist(playlists)
+ libraryViewModel.deleteRoomPlaylist(playlists)
+ libraryViewModel.forceReload(ReloadType.Playlists)
+ }
+ .create()
+ .colorButtons()
+ }
+}
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 47c1cc06e..5fd45b060 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,69 +1,162 @@
+/*
+ * Copyright (c) 2020 Hemanth Savarla.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the 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.Activity
+import android.app.Dialog
+import android.content.Intent
import android.os.Bundle
-import android.text.Html
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import code.name.monkey.appthemehelper.ThemeStore
-import code.name.monkey.appthemehelper.util.MaterialUtil
+import android.provider.MediaStore
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.os.bundleOf
+import androidx.core.text.parseAsHtml
+import androidx.fragment.app.DialogFragment
+import code.name.monkey.appthemehelper.util.VersionUtils
+import code.name.monkey.retromusic.EXTRA_SONG
import code.name.monkey.retromusic.R
+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
+import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.MusicUtil
-import code.name.monkey.retromusic.views.RoundedBottomSheetDialogFragment
-import kotlinx.android.synthetic.main.dialog_delete.*
-import java.util.*
+import code.name.monkey.retromusic.util.SAFUtil
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.getViewModel
-class DeleteSongsDialog : RoundedBottomSheetDialogFragment() {
-
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- title.setTextColor(ThemeStore.textColorPrimary(context!!))
- MaterialUtil.setTint(actionDelete, true)
- MaterialUtil.setTint(actionCancel, false)
-
- //noinspection unchecked,ConstantConditions
- val songs = arguments!!.getParcelableArrayList("songs")
- val content: CharSequence
- if (songs != null) {
- content = if (songs.size > 1) {
- Html.fromHtml(getString(R.string.delete_x_songs, songs.size))
- } else {
- Html.fromHtml(getString(R.string.delete_song_x, songs[0].title))
- }
- this.title.text = content
- }
- actionDelete.setOnClickListener {
- if (songs != null) {
- MusicUtil.deleteTracks(activity!!, songs)
- }
- dismiss()
- }
- actionCancel.setOnClickListener { dismiss() }
- }
-
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
- return inflater.inflate(R.layout.dialog_delete, container, false)
- }
+class DeleteSongsDialog : DialogFragment() {
+ lateinit var libraryViewModel: LibraryViewModel
companion object {
-
fun create(song: Song): DeleteSongsDialog {
val list = ArrayList()
list.add(song)
return create(list)
}
- fun create(songs: ArrayList): DeleteSongsDialog {
- val dialog = DeleteSongsDialog()
- val args = Bundle()
- args.putParcelableArrayList("songs", songs)
- dialog.arguments = args
- return dialog
+ fun create(songs: List): DeleteSongsDialog {
+ return DeleteSongsDialog().apply {
+ arguments = bundleOf(
+ EXTRA_SONG to ArrayList(songs)
+ )
+ }
}
}
-}
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ libraryViewModel = activity?.getViewModel() as LibraryViewModel
+ val songs = extraNotNull>(EXTRA_SONG).value
+ if (VersionUtils.hasR()) {
+ val deleteResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
+ MusicPlayerRemote.playNextSong()
+ }
+ MusicPlayerRemote.removeFromQueue(songs)
+ reloadTabs()
+ }
+ dismiss()
+ }
+ val pendingIntent =
+ MediaStore.createDeleteRequest(requireActivity().contentResolver, songs.map {
+ MusicUtil.getSongFileUri(it.id)
+ })
+ deleteResultLauncher.launch(
+ IntentSenderRequest.Builder(pendingIntent.intentSender).build()
+ )
+ return super.onCreateDialog(savedInstanceState)
+ } else {
+ val pair = if (songs.size > 1) {
+ Pair(
+ R.string.delete_songs_title,
+ String.format(getString(R.string.delete_x_songs), songs.size).parseAsHtml()
+ )
+ } else {
+ Pair(
+ R.string.delete_song_title,
+ String.format(getString(R.string.delete_song_x), songs[0].title).parseAsHtml()
+ )
+ }
+
+ return materialDialog()
+ .title(pair.first)
+ .message(text = pair.second)
+ .noAutoDismiss()
+ .negativeButton(android.R.string.cancel)
+ {
+ dismiss()
+ }
+ .positiveButton(R.string.action_delete)
+ {
+ if ((songs.size == 1) && MusicPlayerRemote.isPlaying(songs[0])) {
+ MusicPlayerRemote.playNextSong()
+ }
+ if (!SAFUtil.isSAFRequiredForSongs(songs)) {
+ CoroutineScope(Dispatchers.IO).launch {
+ dismiss()
+ MusicUtil.deleteTracks(requireContext(), songs)
+ reloadTabs()
+ }
+ } else {
+ if (SAFUtil.isSDCardAccessGranted(requireActivity())) {
+ deleteSongs(songs)
+ } else {
+ startActivityForResult(
+ Intent(requireActivity(), code.name.monkey.retromusic.activities.saf.SAFGuideActivity::class.java),
+ code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when (requestCode) {
+ code.name.monkey.retromusic.activities.saf.SAFGuideActivity.REQUEST_CODE_SAF_GUIDE -> {
+ SAFUtil.openTreePicker(this)
+ }
+ SAFUtil.REQUEST_SAF_PICK_TREE,
+ SAFUtil.REQUEST_SAF_PICK_FILE -> {
+ if (resultCode == Activity.RESULT_OK) {
+ SAFUtil.saveTreeUri(requireActivity(), data)
+ val songs = extraNotNull