├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_OLD.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── asset1.wav │ └── ringtones │ │ └── asset2.mp3 │ ├── java │ └── xyz │ │ └── aprildown │ │ └── ultimateringtonepicker │ │ └── app │ │ ├── MainActivity.kt │ │ └── Toasts.kt │ └── res │ ├── drawable │ └── ic_launcher_foreground.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── raw │ ├── default_ringtone.mp3 │ └── short_message.mp3 │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── art ├── activity.webp ├── dark.webp ├── dialog.webp └── ic_launcher-web.webp ├── build.gradle ├── dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── xyz │ │ └── aprildown │ │ └── ultimateringtonepicker │ │ ├── RingtonePickerActivity.kt │ │ ├── RingtonePickerDialog.kt │ │ ├── RingtonePickerFragment.kt │ │ ├── RingtonePickerViewModel.kt │ │ ├── UltimateRingtonePicker.kt │ │ ├── Utils.kt │ │ ├── data │ │ ├── CustomRingtone.kt │ │ ├── CustomRingtoneDAO.kt │ │ ├── CustomRingtoneModel.kt │ │ ├── DeviceRingtoneModel.kt │ │ ├── Models.kt │ │ ├── SystemRingtoneModel.kt │ │ └── folder │ │ │ ├── RingtoneFolderRetriever.kt │ │ │ ├── RingtoneFolderRetrieverCompat.kt │ │ │ ├── RingtoneFolderRetrieverPreQ.kt │ │ │ └── RingtoneFolderRetrieverQ.kt │ │ ├── music │ │ └── AsyncRingtonePlayer.kt │ │ └── ui │ │ ├── CategoryFragment.kt │ │ ├── DeviceRingtoneFragment.kt │ │ ├── EventHandler.kt │ │ ├── RecyclerViewUtils.kt │ │ ├── RingtoneFragment.kt │ │ ├── SystemRingtoneFragment.kt │ │ ├── VisibleAddCustom.kt │ │ ├── VisibleCategory.kt │ │ ├── VisibleEmptyView.kt │ │ ├── VisibleRingtone.kt │ │ └── VisibleSection.kt │ └── res │ ├── anim-v22 │ └── urp_ringtone_active_animation_interpolator.xml │ ├── animator-v22 │ ├── urp_ringtone_active_outlines_0_animation.xml │ ├── urp_ringtone_active_outlines_1_animation.xml │ └── urp_ringtone_active_outlines_2_animation.xml │ ├── drawable-v22 │ └── urp_ringtone_active_animated.xml │ ├── drawable │ ├── urp_add_custom.xml │ ├── urp_broken_ringtone.xml │ ├── urp_custom_music.xml │ ├── urp_ringtone_active_static.xml │ ├── urp_ringtone_background.xml │ ├── urp_ringtone_normal.xml │ ├── urp_ringtone_selected.xml │ └── urp_ringtone_silent.xml │ ├── layout │ ├── urp_activity_ringtone_picker.xml │ ├── urp_dialog.xml │ ├── urp_empty.xml │ ├── urp_fragment_device_ringtone.xml │ ├── urp_horizontal_divider.xml │ ├── urp_recycler_view.xml │ ├── urp_ringtone.xml │ ├── urp_section.xml │ └── urp_two_lines.xml │ ├── navigation │ └── urp_nav_graph.xml │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-night │ └── colors.xml │ ├── values-nl │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-v22 │ └── drawable.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rHK │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ └── values │ ├── colors.xml │ ├── drawable.xml │ ├── ids.xml │ ├── public.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/java,linux,macos,gradle,kotlin,windows,android,jetbrains,jetbrains+iml,jetbrains+all,androidstudio 3 | # Edit at https://www.gitignore.io/?templates=java,linux,macos,gradle,kotlin,windows,android,jetbrains,jetbrains+iml,jetbrains+all,androidstudio 4 | 5 | ### Android ### 6 | # Built application files 7 | *.apk 8 | *.ap_ 9 | *.aab 10 | 11 | # Files for the ART/Dalvik VM 12 | *.dex 13 | 14 | # Java class files 15 | *.class 16 | 17 | # Generated files 18 | bin/ 19 | gen/ 20 | out/ 21 | release/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | 27 | # Local configuration file (sdk path, etc) 28 | local.properties 29 | 30 | # Proguard folder generated by Eclipse 31 | proguard/ 32 | 33 | # Log Files 34 | *.log 35 | 36 | # Android Studio Navigation editor temp files 37 | .navigation/ 38 | 39 | # Android Studio captures folder 40 | captures/ 41 | 42 | # IntelliJ 43 | *.iml 44 | .idea/workspace.xml 45 | .idea/tasks.xml 46 | .idea/gradle.xml 47 | .idea/assetWizardSettings.xml 48 | .idea/dictionaries 49 | .idea/libraries 50 | # Android Studio 3 in .gitignore file. 51 | .idea/caches 52 | .idea/modules.xml 53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 54 | .idea/navEditor.xml 55 | 56 | # Keystore files 57 | # Uncomment the following lines if you do not want to check your keystore files in. 58 | #*.jks 59 | #*.keystore 60 | 61 | # External native build folder generated in Android Studio 2.2 and later 62 | .externalNativeBuild 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | ### Android Patch ### 90 | gen-external-apklibs 91 | output.json 92 | 93 | ### Java ### 94 | # Compiled class file 95 | 96 | # Log file 97 | 98 | # BlueJ files 99 | *.ctxt 100 | 101 | # Mobile Tools for Java (J2ME) 102 | .mtj.tmp/ 103 | 104 | # Package Files # 105 | *.jar 106 | *.war 107 | *.nar 108 | *.ear 109 | *.zip 110 | *.tar.gz 111 | *.rar 112 | 113 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 114 | hs_err_pid* 115 | 116 | ### JetBrains ### 117 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 118 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 119 | 120 | # User-specific stuff 121 | .idea/**/workspace.xml 122 | .idea/**/tasks.xml 123 | .idea/**/usage.statistics.xml 124 | .idea/**/dictionaries 125 | .idea/**/shelf 126 | 127 | # Generated files 128 | .idea/**/contentModel.xml 129 | 130 | # Sensitive or high-churn files 131 | .idea/**/dataSources/ 132 | .idea/**/dataSources.ids 133 | .idea/**/dataSources.local.xml 134 | .idea/**/sqlDataSources.xml 135 | .idea/**/dynamic.xml 136 | .idea/**/uiDesigner.xml 137 | .idea/**/dbnavigator.xml 138 | 139 | # Gradle 140 | .idea/**/gradle.xml 141 | .idea/**/libraries 142 | 143 | # Gradle and Maven with auto-import 144 | # When using Gradle or Maven with auto-import, you should exclude module files, 145 | # since they will be recreated, and may cause churn. Uncomment if using 146 | # auto-import. 147 | # .idea/modules.xml 148 | # .idea/*.iml 149 | # .idea/modules 150 | # *.iml 151 | # *.ipr 152 | 153 | # CMake 154 | cmake-build-*/ 155 | 156 | # Mongo Explorer plugin 157 | .idea/**/mongoSettings.xml 158 | 159 | # File-based project format 160 | *.iws 161 | 162 | # IntelliJ 163 | 164 | # mpeltonen/sbt-idea plugin 165 | .idea_modules/ 166 | 167 | # JIRA plugin 168 | atlassian-ide-plugin.xml 169 | 170 | # Cursive Clojure plugin 171 | .idea/replstate.xml 172 | 173 | # Crashlytics plugin (for Android Studio and IntelliJ) 174 | com_crashlytics_export_strings.xml 175 | crashlytics.properties 176 | crashlytics-build.properties 177 | fabric.properties 178 | 179 | # Editor-based Rest Client 180 | .idea/httpRequests 181 | 182 | # Android studio 3.1+ serialized cache file 183 | .idea/caches/build_file_checksums.ser 184 | 185 | ### JetBrains Patch ### 186 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 187 | 188 | # *.iml 189 | # modules.xml 190 | # .idea/misc.xml 191 | # *.ipr 192 | 193 | # Sonarlint plugin 194 | .idea/sonarlint 195 | 196 | ### JetBrains+all ### 197 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 198 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 199 | 200 | # User-specific stuff 201 | 202 | # Generated files 203 | 204 | # Sensitive or high-churn files 205 | 206 | # Gradle 207 | 208 | # Gradle and Maven with auto-import 209 | # When using Gradle or Maven with auto-import, you should exclude module files, 210 | # since they will be recreated, and may cause churn. Uncomment if using 211 | # auto-import. 212 | # .idea/modules.xml 213 | # .idea/*.iml 214 | # .idea/modules 215 | # *.iml 216 | # *.ipr 217 | 218 | # CMake 219 | 220 | # Mongo Explorer plugin 221 | 222 | # File-based project format 223 | 224 | # IntelliJ 225 | 226 | # mpeltonen/sbt-idea plugin 227 | 228 | # JIRA plugin 229 | 230 | # Cursive Clojure plugin 231 | 232 | # Crashlytics plugin (for Android Studio and IntelliJ) 233 | 234 | # Editor-based Rest Client 235 | 236 | # Android studio 3.1+ serialized cache file 237 | 238 | ### JetBrains+all Patch ### 239 | # Ignores the whole .idea folder and all .iml files 240 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 241 | 242 | .idea/ 243 | 244 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 245 | 246 | modules.xml 247 | .idea/misc.xml 248 | *.ipr 249 | 250 | # Sonarlint plugin 251 | 252 | ### JetBrains+iml ### 253 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 254 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 255 | 256 | # User-specific stuff 257 | 258 | # Generated files 259 | 260 | # Sensitive or high-churn files 261 | 262 | # Gradle 263 | 264 | # Gradle and Maven with auto-import 265 | # When using Gradle or Maven with auto-import, you should exclude module files, 266 | # since they will be recreated, and may cause churn. Uncomment if using 267 | # auto-import. 268 | # .idea/modules.xml 269 | # .idea/*.iml 270 | # .idea/modules 271 | # *.iml 272 | # *.ipr 273 | 274 | # CMake 275 | 276 | # Mongo Explorer plugin 277 | 278 | # File-based project format 279 | 280 | # IntelliJ 281 | 282 | # mpeltonen/sbt-idea plugin 283 | 284 | # JIRA plugin 285 | 286 | # Cursive Clojure plugin 287 | 288 | # Crashlytics plugin (for Android Studio and IntelliJ) 289 | 290 | # Editor-based Rest Client 291 | 292 | # Android studio 3.1+ serialized cache file 293 | 294 | ### JetBrains+iml Patch ### 295 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 296 | 297 | 298 | ### Kotlin ### 299 | # Compiled class file 300 | 301 | # Log file 302 | 303 | # BlueJ files 304 | 305 | # Mobile Tools for Java (J2ME) 306 | 307 | # Package Files # 308 | 309 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 310 | 311 | ### Linux ### 312 | *~ 313 | 314 | # temporary files which can be created if a process still has a handle open of a deleted file 315 | .fuse_hidden* 316 | 317 | # KDE directory preferences 318 | .directory 319 | 320 | # Linux trash folder which might appear on any partition or disk 321 | .Trash-* 322 | 323 | # .nfs files are created when an open file is removed but is still being accessed 324 | .nfs* 325 | 326 | ### macOS ### 327 | # General 328 | .DS_Store 329 | .AppleDouble 330 | .LSOverride 331 | 332 | # Icon must end with two \r 333 | Icon 334 | 335 | # Thumbnails 336 | ._* 337 | 338 | # Files that might appear in the root of a volume 339 | .DocumentRevisions-V100 340 | .fseventsd 341 | .Spotlight-V100 342 | .TemporaryItems 343 | .Trashes 344 | .VolumeIcon.icns 345 | .com.apple.timemachine.donotpresent 346 | 347 | # Directories potentially created on remote AFP share 348 | .AppleDB 349 | .AppleDesktop 350 | Network Trash Folder 351 | Temporary Items 352 | .apdisk 353 | 354 | ### Windows ### 355 | # Windows thumbnail cache files 356 | Thumbs.db 357 | Thumbs.db:encryptable 358 | ehthumbs.db 359 | ehthumbs_vista.db 360 | 361 | # Dump file 362 | *.stackdump 363 | 364 | # Folder config file 365 | [Dd]esktop.ini 366 | 367 | # Recycle Bin used on file shares 368 | $RECYCLE.BIN/ 369 | 370 | # Windows Installer files 371 | *.cab 372 | *.msi 373 | *.msix 374 | *.msm 375 | *.msp 376 | 377 | # Windows shortcuts 378 | *.lnk 379 | 380 | ### Gradle ### 381 | .gradle 382 | 383 | # Ignore Gradle GUI config 384 | gradle-app.setting 385 | 386 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 387 | !gradle-wrapper.jar 388 | 389 | # Cache of project 390 | .gradletasknamecache 391 | 392 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 393 | # gradle/wrapper/gradle-wrapper.properties 394 | 395 | ### Gradle Patch ### 396 | **/build/ 397 | 398 | ### AndroidStudio ### 399 | # Covers files to be ignored for android development using Android Studio. 400 | 401 | # Built application files 402 | 403 | # Files for the ART/Dalvik VM 404 | 405 | # Java class files 406 | 407 | # Generated files 408 | 409 | # Gradle files 410 | 411 | # Signing files 412 | .signing/ 413 | 414 | # Local configuration file (sdk path, etc) 415 | 416 | # Proguard folder generated by Eclipse 417 | 418 | # Log Files 419 | 420 | # Android Studio 421 | /*/build/ 422 | /*/local.properties 423 | /*/out 424 | /*/*/build 425 | /*/*/production 426 | *.swp 427 | 428 | # Android Patch 429 | 430 | # External native build folder generated in Android Studio 2.2 and later 431 | 432 | # NDK 433 | obj/ 434 | 435 | # IntelliJ IDEA 436 | /out/ 437 | 438 | # User-specific configurations 439 | .idea/caches/ 440 | .idea/libraries/ 441 | .idea/shelf/ 442 | .idea/.name 443 | .idea/compiler.xml 444 | .idea/copyright/profiles_settings.xml 445 | .idea/encodings.xml 446 | .idea/scopes/scope_settings.xml 447 | .idea/vcs.xml 448 | .idea/jsLibraryMappings.xml 449 | .idea/datasources.xml 450 | .idea/dataSources.ids 451 | .idea/sqlDataSources.xml 452 | .idea/dynamic.xml 453 | .idea/uiDesigner.xml 454 | 455 | # OS-specific files 456 | .DS_Store? 457 | 458 | # Legacy Eclipse project files 459 | .classpath 460 | .project 461 | .cproject 462 | .settings/ 463 | 464 | # Mobile Tools for Java (J2ME) 465 | 466 | # Package Files # 467 | 468 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 469 | 470 | ## Plugin-specific files: 471 | 472 | # mpeltonen/sbt-idea plugin 473 | 474 | # JIRA plugin 475 | 476 | # Mongo Explorer plugin 477 | .idea/mongoSettings.xml 478 | 479 | # Crashlytics plugin (for Android Studio and IntelliJ) 480 | 481 | ### AndroidStudio Patch ### 482 | 483 | !/gradle/wrapper/gradle-wrapper.jar 484 | 485 | # End of https://www.gitignore.io/api/java,linux,macos,gradle,kotlin,windows,android,jetbrains,jetbrains+iml,jetbrains+all,androidstudio 486 | 487 | *.aar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DeweyReed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

UltimateRingtonePicker

6 | 7 |
8 | Pick ringtone, notification, alarm sound and ringtone files from external storage with an activity or a dialog 9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | API 23 | 24 |
25 |
26 | 27 | ## Features 28 | 29 | - Respects Scoped Storage(MediaStore is used) 30 | - Available as an Activity and a Dialog 31 | - Options to pick alarm sound, notification sound, ringtone sound, and external ringtones. 32 | - Ringtone preview 33 | - An interface to set a default entry 34 | - An interface to add custom ringtone entries 35 | - Sorted external ringtones with artists, albums and folders 36 | - Automatically remembers which external ringtones users have picked 37 | - Multi-select 38 | - Dark theme support out of box 39 | - Permissions are handled internally 40 | - Storage Access Framework support 41 | 42 | The library is inspired by [AOSP DeskClock RingtonePickerActivity](https://android.googlesource.com/platform/packages/apps/DeskClock/+/refs/heads/master/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt). 43 | 44 | ## Screenshot 45 | 46 | |||| 47 | |:-:|:-:|:-:| 48 | |![Activity](./art/activity.webp)|![Dialog](./art/dialog.webp)|![Dark](./art/dark.webp)| 49 | 50 | ## Gradle Dependency 51 | 52 | Step 1. Add the JitPack repository to your build file 53 | 54 | Add it in your root build.gradle at the end of repositories: 55 | 56 | ```Groovy 57 | allprojects { 58 | repositories { 59 | maven { url 'https://jitpack.io' } 60 | } 61 | } 62 | ``` 63 | 64 | Step 2. Add the dependency 65 | 66 | [![The Newest Version](https://jitpack.io/v/com.github.DeweyReed/UltimateRingtonePicker.svg)](https://jitpack.io/#com.github.DeweyReed/UltimateRingtonePicker) 67 | 68 | ```Groovy 69 | dependencies { 70 | implementation 'com.github.DeweyReed:UltimateRingtonePicker:3.2.0' 71 | } 72 | ``` 73 | 74 | ## Usage 75 | 76 | [Demo APK](https://github.com/deweyreed/ultimateringtonepicker/releases) and [examples in the MainActivity](./app/src/main/java/xyz/aprildown/ultimateringtonepicker/app/MainActivity.kt). 77 | 78 | ### 0. Add Permission 79 | 80 | Add `` 81 | or `` when targeting Android 82 | 13 to your Manifest if you are not going to use Storage Access Framework. 83 | 84 | ### 1. Create an `UltimateRingtonePicker.Settings` 85 | 86 | ```Kotlin 87 | val settings = UltimateRingtonePicker.Settings( 88 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 89 | customSection = UltimateRingtonePicker.SystemRingtonePicker.CustomSection(), 90 | defaultSection = UltimateRingtonePicker.SystemRingtonePicker.DefaultSection(), 91 | ringtoneTypes = listOf( 92 | RingtoneManager.TYPE_RINGTONE, 93 | RingtoneManager.TYPE_NOTIFICATION, 94 | RingtoneManager.TYPE_ALARM 95 | ) 96 | ), 97 | deviceRingtonePicker = UltimateRingtonePicker.DeviceRingtonePicker( 98 | deviceRingtoneTypes = listOf( 99 | UltimateRingtonePicker.RingtoneCategoryType.All, 100 | UltimateRingtonePicker.RingtoneCategoryType.Artist, 101 | UltimateRingtonePicker.RingtoneCategoryType.Album, 102 | UltimateRingtonePicker.RingtoneCategoryType.Folder 103 | ) 104 | ) 105 | ) 106 | ``` 107 | 108 | ### 2. Launch the picker 109 | 110 | - Launch the Activity picker 111 | 112 | 1. Add the Activity to the manifest. 113 | 114 | `` 115 | 116 | 1. Register Activity Result callback 117 | 118 | ```Kotlin 119 | rivate val ringtoneLauncher = 120 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 121 | if (it.resultCode == Activity.RESULT_OK && it.data != null) { 122 | val ringtones = RingtonePickerActivity.getPickerResult(data) 123 | } 124 | } 125 | ``` 126 | 127 | 1. Start Activity 128 | 129 | ```Kotlin 130 | ringtoneLauncher.launch( 131 | RingtonePickerActivity.getIntent( 132 | context = this, 133 | settings = settings, 134 | windowTitle = "Activity Picker" 135 | ) 136 | ) 137 | ``` 138 | 139 | - Launch the dialog picker 140 | 141 | 1. Show the dialog 142 | 143 | ```Kotlin 144 | RingtonePickerDialog.createInstance( 145 | settings = settings, 146 | dialogTitle = "Dialog!" 147 | ).show(supportFragmentManager, null) 148 | ``` 149 | 150 | 1. Get the result 151 | 152 | Implement `UltimateRingtonePicker.RingtonePickerListener` in your activity or fragment. 153 | 154 | ```Kotlin 155 | override fun onRingtonePicked(ringtones: List) { 156 | 157 | } 158 | ``` 159 | 160 | Alternatively, you can launch the dialog and get the result without implementing the interface, but the dialog will be dismissed in `onPause`: 161 | 162 | ```Kotlin 163 | RingtonePickerDialog.createEphemeralInstance( 164 | settings = settings, 165 | dialogTitle = "Dialog", 166 | listener = object : UltimateRingtonePicker.RingtonePickerListener { 167 | override fun onRingtonePicked(ringtones: List) { 168 | 169 | } 170 | } 171 | ).show(supportFragmentManager, null) 172 | ``` 173 | 174 | ## BTW 175 | 176 | `UltimateRingtonePicker` supports activity pick `RingtonePickerActivity` and dialog pick `RingtonePickerDialog` out of the box. Both of them are just wrappers of `RingtonePickerFragment`. Therefore, you can directly wrap `RingtonePickerFragment` into your activity/fragment to provide more customization! 177 | 178 | ## License 179 | 180 | [MIT License](./LICENSE) 181 | -------------------------------------------------------------------------------- /README_OLD.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

UltimateMusicPicker

6 | 7 |
8 | Pick ringtone, notification, alarm sound and music files from external storage with an Activity or a dialog 9 |
10 |
11 | 29 |
30 | 31 | ## Overview 32 | 33 | - Separates music to alarm sound, notification sound, ringtone sound and external music files. 34 | - Provides interface to specify default item 35 | - Provides interface to add custom music items 36 | - Automatically remembers which external music files users picked 37 | - Music Preview 38 | - Available as an Activity and a Dialog 39 | - Dark theme support 40 | - Permission are handled internally 41 | - AndroidX support 42 | 43 | # Table of Contents 44 | 45 | 1. [Sample APK](https://github.com/DeweyReed/UltimateMusicPicker/releases) 46 | 1. [Gradle Dependency](#gradle-dependency) 47 | 1. [Usage](#usage) 48 | 1. [Advanced Usage](#advanced-usage) 49 | 1. [Custom Activity](#custom-activity) 50 | 1. [Dark Theme](#dark-theme) 51 | 1. [Todo List](#todo-list) 52 | 1. [Migrate from 1.X](#migrate-from-1x) 53 | 1. [License](#license) 54 | 55 | ## Gradle Dependency 56 | 57 | Step 1. Add the JitPack repository to your build file 58 | 59 | Add it in your root build.gradle at the end of repositories: 60 | 61 | ```Groovy 62 | allprojects { 63 | repositories { 64 | ... 65 | maven { url 'https://jitpack.io' } 66 | } 67 | } 68 | ``` 69 | 70 | Step 2. Add the dependency 71 | 72 | ```Groovy 73 | dependencies { 74 | implementation 'com.github.DeweyReed:UltimateMusicPicker:2.0.6' 75 | } 76 | ``` 77 | 78 | ## Usage 79 | 80 | 1. If you wish to pick external music files, add `READ_EXTERNAL_STORAGE` permission to the `Manifest.xml`. 81 | 82 | `` 83 | 84 | 1. If you wish to use picker dialog, implement `MusicPickerListener` in the activity or fragment to get pick result. 85 | 86 | ```Kotlin 87 | interface MusicPickerListener { 88 | fun onMusicPick(uri: Uri, title: String) 89 | fun onPickCanceled() 90 | } 91 | ``` 92 | 93 | 1. If you wish to use predefined activity to pick music, add this to the `Manifest.xml`: 94 | 95 | `` 96 | 97 | and receive the pick result in the `onActivityResult`: 98 | 99 | ```Kotlin 100 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 101 | if (resultCode == Activity.RESULT_OK) { 102 | val title = data?.getStringExtra(UltimateMusicPicker.EXTRA_SELECTED_TITLE) 103 | val uri = data?.getParcelableExtra(UltimateMusicPicker.EXTRA_SELECTED_URI) 104 | if (title != null && uri != null) { 105 | onMusicPick(uri, title) 106 | } else { 107 | onPickCanceled() 108 | } 109 | } else super.onActivityResult(requestCode, resultCode, data) 110 | } 111 | ``` 112 | 113 | 1. Let's pick some something. 114 | 115 | ```Kotlin 116 | UltimateMusicPicker() 117 | // Picker activity action bar title or dialog title 118 | .windowTitle("UltimateMusicPicker") 119 | 120 | // Add a extra default item 121 | .defaultUri(uri) 122 | // Add a default item and change the default item name("Default" is used otherwise) 123 | .defaultTitleAndUri("My default name", uri) 124 | 125 | // There's a "silent" item by default, use this line to remove it. 126 | .removeSilent() 127 | 128 | // Select this uri 129 | .selectUri(uri) 130 | 131 | // Add some other music items(from R.raw or app's asset) 132 | .additional("Myself Music", uri) 133 | .additional("Another Music", uri) 134 | 135 | // Music preview stream type(AudioManager.STREAM_MUSIC is used by default) 136 | .streamType(AudioManager.STREAM_ALARM) 137 | 138 | // Show different kinds of system ringtones. Calling order determines their display order. 139 | .ringtone() 140 | .notification() 141 | .alarm() 142 | // Show music files from external storage. Requires READ_EXTERNAL_STORAGE permission. 143 | .music() 144 | 145 | // Show a picker dialog 146 | .goWithDialog(supportFragmentManager) 147 | // Or show a picker activity 148 | //.goWithActivity(this, 0, MusicPickerActivity::class.java) 149 | ``` 150 | 151 | **When you launch dialog picker in an fragment, remember to use `childFragmentManager` instead of `fragmentManager` to make sure the child can find his/her parents.** 152 | 153 | ## Advanced Usage 154 | 155 | The picker view is a `Fragment` so it can be easily used in an Activity and a dialog. 156 | 157 | ### Custom Activity 158 | 159 | Simply copy and paste [`MusicPickerActivity`](https://github.com/DeweyReed/UltimateMusicPicker/blob/master/library/src/main/java/xyz/aprildown/ringtone/MusicPickerActivity.kt) or [`MusicPickerDialog`](https://github.com/DeweyReed/UltimateMusicPicker/blob/master/library/src/main/java/xyz/aprildown/ringtone/MusicPickerDialog.kt) and create your own. You may notice it's just a wrapper for `MusicPickerFragment` and it can be used in many places(like in a `ViewPager`?) 160 | 161 | What's more, there are two methods in the `UltimateMusicPicker` class to help you. 162 | 163 | ```Kotlin 164 | /** 165 | * Create a setting [Parcelable]. Useful when customize how to start activity 166 | */ 167 | fun buildParcelable(): Parcelable 168 | 169 | /** 170 | * Put a setting [Parcelable] into a [Intent]. Useful when customize how to start activity 171 | */ 172 | fun putSettingIntoIntent(intent: Intent): Intent 173 | ``` 174 | 175 | ### Dark Theme 176 | 177 | This library supports dark theme with a naive way. It works fine when I use `AppCompatDelegate.setDefaultNightMode` to toggle night theme. If this is not enough, open a issue or send a PR. 178 | 179 | ## Todo List 180 | 181 | - Use `READ_CONTENT` to select without permission 182 | 183 | ## Migrate from 1.X 184 | 185 | 2.0.0 renames package name from `xyz.aprildown.ringtone.UltimateMusicPicker` to `xyz.aprildown.ultimatemusicpicker.UltimateMusicPicker` to make it more meaningful. 186 | 187 | So after cleaning up imports, everything should work. 188 | 189 | ## License 190 | 191 | [MIT License](https://github.com/DeweyReed/UltimateMusicPicker/blob/master/LICENSE) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('com.android.application') 3 | 4 | id('kotlin-android') 5 | } 6 | 7 | android { 8 | namespace 'xyz.aprildown.ultimateringtonepicker.app' 9 | 10 | compileSdkVersion versions.compile_sdk 11 | 12 | defaultConfig { 13 | applicationId "xyz.aprildown.ultimateringtonepicker.app" 14 | minSdkVersion versions.min_sdk 15 | targetSdkVersion versions.target_sdk 16 | versionCode versions.version_code 17 | versionName versions.version_name 18 | vectorDrawables.useSupportLibrary true 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled true 24 | shrinkResources true 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_17 31 | targetCompatibility = JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = JavaVersion.VERSION_17.toString() 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation project(':library') 40 | 41 | debugImplementation libs.leak_cannary 42 | 43 | implementation libs.androidx_appcompat 44 | implementation libs.material 45 | 46 | implementation libs.easy_permission 47 | } 48 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/assets/asset1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeweyReed/UltimateRingtonePicker/ed873ed1cbdcc76c7bcac63b1f6323cecc888e91/app/src/main/assets/asset1.wav -------------------------------------------------------------------------------- /app/src/main/assets/ringtones/asset2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeweyReed/UltimateRingtonePicker/ed873ed1cbdcc76c7bcac63b1f6323cecc888e91/app/src/main/assets/ringtones/asset2.mp3 -------------------------------------------------------------------------------- /app/src/main/java/xyz/aprildown/ultimateringtonepicker/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.aprildown.ultimateringtonepicker.app 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.media.RingtoneManager 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.os.Handler 11 | import android.os.Looper 12 | import android.provider.Settings 13 | import android.view.View 14 | import androidx.activity.result.contract.ActivityResultContracts 15 | import androidx.appcompat.app.AppCompatActivity 16 | import pub.devrel.easypermissions.AfterPermissionGranted 17 | import pub.devrel.easypermissions.EasyPermissions 18 | import pub.devrel.easypermissions.PermissionRequest 19 | import xyz.aprildown.ultimateringtonepicker.RingtonePickerActivity 20 | import xyz.aprildown.ultimateringtonepicker.RingtonePickerDialog 21 | import xyz.aprildown.ultimateringtonepicker.UltimateRingtonePicker 22 | import java.io.File 23 | 24 | @Suppress("UNUSED_PARAMETER") 25 | class MainActivity : AppCompatActivity(), UltimateRingtonePicker.RingtonePickerListener { 26 | 27 | private var currentSelectedRingtones = listOf() 28 | 29 | private val ringtoneLauncher = 30 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 31 | if (it.resultCode == Activity.RESULT_OK && it.data != null) { 32 | handleResult(RingtonePickerActivity.getPickerResult(it.data)) 33 | } 34 | } 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | setContentView(R.layout.activity_main) 39 | } 40 | 41 | fun openPermission(view: View) { 42 | startActivity( 43 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 44 | .setData(Uri.parse("package:${packageName}")) 45 | ) 46 | } 47 | 48 | fun openStandardActivity(view: View) { 49 | ringtoneLauncher.launch( 50 | RingtonePickerActivity.getIntent( 51 | context = this, 52 | settings = createStandardSettings(), 53 | windowTitle = "Picker Picker" 54 | ) 55 | ) 56 | } 57 | 58 | fun openStandardDialog(view: View) { 59 | RingtonePickerDialog.createInstance( 60 | createStandardSettings(), 61 | "Dialog!" 62 | ).show(supportFragmentManager, null) 63 | } 64 | 65 | fun emulateSystemDialog(view: View) { 66 | RingtonePickerDialog.createInstance( 67 | UltimateRingtonePicker.Settings( 68 | preSelectUris = currentSelectedRingtones.map { it.uri }, 69 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 70 | ringtoneTypes = listOf( 71 | RingtoneManager.TYPE_RINGTONE, 72 | RingtoneManager.TYPE_NOTIFICATION, 73 | RingtoneManager.TYPE_ALARM 74 | ) 75 | ) 76 | ), 77 | "System Ringtones" 78 | ).show(supportFragmentManager, null) 79 | } 80 | 81 | fun showOnlyDeviceRingtones(view: View) { 82 | val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 83 | Manifest.permission.READ_MEDIA_AUDIO 84 | } else { 85 | Manifest.permission.READ_EXTERNAL_STORAGE 86 | } 87 | if (EasyPermissions.hasPermissions(this, permission)) { 88 | pickDeviceRingtones() 89 | } else { 90 | EasyPermissions.requestPermissions( 91 | PermissionRequest.Builder(this, 0, permission).build() 92 | ) 93 | } 94 | } 95 | 96 | override fun onRequestPermissionsResult( 97 | requestCode: Int, 98 | permissions: Array, 99 | grantResults: IntArray 100 | ) { 101 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 102 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) 103 | } 104 | 105 | @Suppress("unused") 106 | @AfterPermissionGranted(0) 107 | fun onGetPermission() { 108 | pickDeviceRingtones() 109 | } 110 | 111 | private fun pickDeviceRingtones() { 112 | RingtonePickerDialog.createInstance( 113 | UltimateRingtonePicker.Settings( 114 | preSelectUris = currentSelectedRingtones.map { it.uri }, 115 | deviceRingtonePicker = UltimateRingtonePicker.DeviceRingtonePicker( 116 | deviceRingtoneTypes = listOf( 117 | UltimateRingtonePicker.RingtoneCategoryType.All, 118 | UltimateRingtonePicker.RingtoneCategoryType.Artist, 119 | UltimateRingtonePicker.RingtoneCategoryType.Album, 120 | UltimateRingtonePicker.RingtoneCategoryType.Folder 121 | ) 122 | ) 123 | ), 124 | "All Device Ringtones" 125 | ).show(supportFragmentManager, null) 126 | } 127 | 128 | fun useAdditionalRingtones(view: View) { 129 | ringtoneLauncher.launch( 130 | RingtonePickerActivity.getIntent( 131 | context = this, 132 | settings = UltimateRingtonePicker.Settings( 133 | preSelectUris = currentSelectedRingtones.map { it.uri }, 134 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 135 | defaultSection = UltimateRingtonePicker.SystemRingtonePicker.DefaultSection( 136 | defaultUri = UltimateRingtonePicker.createRawRingtoneUri( 137 | this, 138 | R.raw.default_ringtone 139 | ), 140 | defaultTitle = "Default Ringtone", 141 | additionalRingtones = listOf( 142 | UltimateRingtonePicker.RingtoneEntry( 143 | UltimateRingtonePicker.createRawRingtoneUri( 144 | this, 145 | R.raw.short_message 146 | ), 147 | "R.raw.short_message" 148 | ), 149 | UltimateRingtonePicker.RingtoneEntry( 150 | UltimateRingtonePicker.createAssetRingtoneUri("asset1.wav"), 151 | "Assets/asset1.mp3" 152 | ), 153 | UltimateRingtonePicker.RingtoneEntry( 154 | UltimateRingtonePicker.createAssetRingtoneUri("ringtones${File.separator}asset2.mp3"), 155 | "Assets/ringtones/asset2.mp3" 156 | ) 157 | ) 158 | ) 159 | ) 160 | 161 | ), 162 | windowTitle = "Additional" 163 | ) 164 | ) 165 | } 166 | 167 | fun enableMultiSelect(view: View) { 168 | ringtoneLauncher.launch( 169 | RingtonePickerActivity.getIntent( 170 | context = this, 171 | settings = UltimateRingtonePicker.Settings( 172 | preSelectUris = currentSelectedRingtones.map { it.uri }, 173 | enableMultiSelect = true, 174 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 175 | customSection = UltimateRingtonePicker.SystemRingtonePicker.CustomSection(), 176 | defaultSection = UltimateRingtonePicker.SystemRingtonePicker.DefaultSection( 177 | defaultUri = UltimateRingtonePicker.createRawRingtoneUri( 178 | this, 179 | R.raw.default_ringtone 180 | ) 181 | ), 182 | ringtoneTypes = listOf( 183 | RingtoneManager.TYPE_RINGTONE, 184 | RingtoneManager.TYPE_NOTIFICATION, 185 | RingtoneManager.TYPE_ALARM 186 | ) 187 | ), 188 | deviceRingtonePicker = UltimateRingtonePicker.DeviceRingtonePicker( 189 | deviceRingtoneTypes = listOf( 190 | UltimateRingtonePicker.RingtoneCategoryType.All, 191 | UltimateRingtonePicker.RingtoneCategoryType.Artist, 192 | UltimateRingtonePicker.RingtoneCategoryType.Album, 193 | UltimateRingtonePicker.RingtoneCategoryType.Folder 194 | ) 195 | ) 196 | ), 197 | windowTitle = "Multi Select" 198 | ) 199 | ) 200 | } 201 | 202 | fun safPick(view: View) { 203 | RingtonePickerDialog.createInstance( 204 | UltimateRingtonePicker.Settings( 205 | preSelectUris = currentSelectedRingtones.map { it.uri }, 206 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 207 | customSection = UltimateRingtonePicker.SystemRingtonePicker.CustomSection( 208 | useSafSelect = true 209 | ), 210 | ringtoneTypes = listOf( 211 | RingtoneManager.TYPE_RINGTONE, 212 | RingtoneManager.TYPE_NOTIFICATION, 213 | RingtoneManager.TYPE_ALARM 214 | ) 215 | ) 216 | ), 217 | "All Device Ringtones" 218 | ).show(supportFragmentManager, null) 219 | } 220 | 221 | fun onlySafPick(view: View) { 222 | ringtoneLauncher.launch( 223 | RingtonePickerActivity.getIntent( 224 | context = this, 225 | settings = UltimateRingtonePicker.Settings( 226 | deviceRingtonePicker = UltimateRingtonePicker.DeviceRingtonePicker( 227 | alwaysUseSaf = true 228 | ) 229 | ), 230 | windowTitle = "SAF!" 231 | ) 232 | ) 233 | } 234 | 235 | fun ephemeralDialog(view: View) { 236 | RingtonePickerDialog.createEphemeralInstance( 237 | createStandardSettings(), 238 | "Ephemeral Dialog", 239 | object : UltimateRingtonePicker.RingtonePickerListener { 240 | override fun onRingtonePicked(ringtones: List) { 241 | toast("Ephemeral!") 242 | Handler(Looper.getMainLooper()).postDelayed({ 243 | handleResult(ringtones) 244 | }, 1000) 245 | } 246 | } 247 | ).show(supportFragmentManager, null) 248 | } 249 | 250 | private fun createStandardSettings(): UltimateRingtonePicker.Settings = 251 | UltimateRingtonePicker.Settings( 252 | preSelectUris = currentSelectedRingtones.map { it.uri }, 253 | loop = false, 254 | systemRingtonePicker = UltimateRingtonePicker.SystemRingtonePicker( 255 | customSection = UltimateRingtonePicker.SystemRingtonePicker.CustomSection(), 256 | defaultSection = UltimateRingtonePicker.SystemRingtonePicker.DefaultSection( 257 | showSilent = true, 258 | defaultUri = UltimateRingtonePicker.createRawRingtoneUri( 259 | this, 260 | R.raw.default_ringtone 261 | ), 262 | defaultTitle = "Default Ringtone", 263 | additionalRingtones = listOf( 264 | UltimateRingtonePicker.RingtoneEntry( 265 | UltimateRingtonePicker.createRawRingtoneUri(this, R.raw.short_message), 266 | "R.raw.short_message" 267 | ) 268 | ) 269 | ), 270 | ringtoneTypes = listOf( 271 | RingtoneManager.TYPE_RINGTONE, 272 | RingtoneManager.TYPE_NOTIFICATION, 273 | RingtoneManager.TYPE_ALARM 274 | ) 275 | ), 276 | deviceRingtonePicker = UltimateRingtonePicker.DeviceRingtonePicker( 277 | deviceRingtoneTypes = listOf( 278 | UltimateRingtonePicker.RingtoneCategoryType.All, 279 | UltimateRingtonePicker.RingtoneCategoryType.Artist, 280 | UltimateRingtonePicker.RingtoneCategoryType.Album, 281 | UltimateRingtonePicker.RingtoneCategoryType.Folder 282 | ) 283 | ) 284 | ) 285 | 286 | override fun onRingtonePicked(ringtones: List) { 287 | handleResult(ringtones) 288 | } 289 | 290 | private fun handleResult(ringtones: List) { 291 | currentSelectedRingtones = ringtones 292 | toast(ringtones.joinToString(separator = "\n") { it.name }) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/aprildown/ultimateringtonepicker/app/Toasts.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE", "unused") 2 | 3 | package xyz.aprildown.ultimateringtonepicker.app 4 | 5 | import android.content.Context 6 | import android.widget.Toast 7 | import androidx.fragment.app.Fragment 8 | 9 | // region Short Toast 10 | 11 | inline fun Context.toast(message: Int): Toast = Toast 12 | .makeText(this, message, Toast.LENGTH_SHORT) 13 | .apply { 14 | show() 15 | } 16 | 17 | inline fun Context.toast(message: CharSequence): Toast = Toast 18 | .makeText(this, message, Toast.LENGTH_SHORT) 19 | .apply { 20 | show() 21 | } 22 | 23 | // endregion 24 | 25 | // region Long Toast 26 | 27 | inline fun Context.longToast(message: Int): Toast = Toast 28 | .makeText(this, message, Toast.LENGTH_LONG) 29 | .apply { 30 | show() 31 | } 32 | 33 | inline fun Context.longToast(message: CharSequence): Toast = Toast 34 | .makeText(this, message, Toast.LENGTH_LONG) 35 | .apply { 36 | show() 37 | } 38 | 39 | // endregion 40 | 41 | // region Fragment 42 | 43 | inline fun Fragment.toast(message: Int): Toast = requireContext().toast(message) 44 | inline fun Fragment.longToast(message: Int): Toast = requireContext().longToast(message) 45 | 46 | // endregion 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 |