From 542489f61b51cf3733edb6e4613f1b6188bfe18a Mon Sep 17 00:00:00 2001 From: Lucas Josino Date: Wed, 29 Mar 2023 16:59:38 -0300 Subject: [PATCH] Update 2.7.0 (#105) --- on_audio_query/CHANGELOG.md | 164 +++++--- on_audio_query/README.md | 234 +++--------- on_audio_query/README.pt-BR.md | 352 ------------------ .../on_audio_query/OnAudioQueryPlugin.kt | 252 ++++++------- .../on_audio_query/PluginProvider.kt | 90 +++++ .../on_audio_query/consts/Method.kt | 29 ++ .../controller/OnAudioController.kt | 41 -- .../controllers/MethodController.kt | 32 ++ .../controllers/PermissionController.kt | 93 +++++ .../PlaylistController.kt} | 24 +- .../OnPermissionManagerInterface.kt | 10 - .../interfaces/PermissionManagerInterface.kt | 7 + .../AlbumQuery.kt} | 87 ++--- .../on_audio_query/queries/AllPathQuery.kt | 60 +++ .../ArtistQuery.kt} | 76 ++-- .../ArtworkQuery.kt} | 138 ++++--- .../AudioFromQuery.kt} | 173 ++++----- .../on_audio_query/queries/AudioQuery.kt | 102 +++++ .../GenreQuery.kt} | 75 ++-- .../PlaylistQuery.kt} | 76 ++-- .../WithFiltersQuery.kt} | 83 ++--- .../helper/QueryHelper.kt} | 4 +- .../on_audio_query/query/OnAllPathQuery.kt | 65 ---- .../on_audio_query/query/OnAudiosQuery.kt | 179 --------- .../on_audio_query/types/ArtworkType.kt | 2 +- ...rsorProjections.kt => CursorProjection.kt} | 0 .../on_audio_query/utils/OnDeviceInfo.kt | 12 - .../android/app/src/main/AndroidManifest.xml | 9 +- on_audio_query/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 78 ++-- on_audio_query/example/ios/Runner/Info.plist | 4 +- on_audio_query/example/lib/main.dart | 132 ++++--- on_audio_query/example/pubspec.yaml | 4 +- .../ios/Classes/PluginProvider.swift | 46 +++ .../ios/Classes/SwiftOnAudioQueryPlugin.swift | 80 ++-- .../ios/Classes/consts/Method.swift | 27 ++ .../controller/OnAudioController.swift | 52 --- .../controller/OnPlaylistsController.swift | 86 ----- .../controllers/MethodController.swift | 45 +++ .../controllers/PermissionController.swift | 25 ++ .../controllers/PlaylistController.swift | 89 +++++ .../ios/Classes/queries/AlbumQuery.swift | 58 +++ .../ios/Classes/queries/ArtistQuery.swift | 51 +++ .../ios/Classes/queries/ArtworkQuery.swift | 127 +++++++ .../AudioFromQuery.swift} | 77 ++-- .../AudioQuery.swift} | 40 +- .../GenreQuery.swift} | 33 +- .../PlaylistQuery.swift} | 33 +- .../WithFiltersQuery.swift} | 91 +++-- .../helper/OnAudioHelper.swift | 80 ++-- .../ios/Classes/query/OnAlbumsQuery.swift | 66 ---- .../ios/Classes/query/OnArtistQuery.swift | 64 ---- .../ios/Classes/query/OnArtworkQuery.swift | 121 ------ .../ios/Classes/types/AudiosFromType.swift | 14 +- .../ios/Classes/types/WithFiltersType.swift | 18 +- on_audio_query/ios/Classes/utils/Log.swift | 40 ++ .../ios/Classes/utils/OnDeviceInfo.swift | 14 - on_audio_query/ios/on_audio_query.podspec | 7 +- .../details/on_audio_query_controller.dart | 44 ++- on_audio_query/lib/on_audio_query.dart | 2 +- .../lib/widget/query_artwork_widget.dart | 114 +++--- on_audio_query/pubspec.yaml | 11 +- .../CHANGELOG.md | 3 + .../lib/details/log/log_config.dart | 16 + .../lib/details/on_audio_query_helper.dart | 4 + .../lib/details/types/log_type.dart | 50 +++ .../lib/method_channel_on_audio_query.dart | 21 +- .../on_audio_query_platform_interface.dart | 18 +- .../pubspec.yaml | 4 +- on_audio_query_web/CHANGELOG.md | 3 + .../lib/on_audio_query_web.dart | 1 - on_audio_query_web/pubspec.yaml | 7 +- 72 files changed, 2082 insertions(+), 2289 deletions(-) delete mode 100644 on_audio_query/README.pt-BR.md create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt delete mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnAudioController.kt create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{controller/OnPlaylistsController.kt => controllers/PlaylistController.kt} (88%) delete mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/OnPermissionManagerInterface.kt create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnAlbumsQuery.kt => queries/AlbumQuery.kt} (52%) create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnArtistsQuery.kt => queries/ArtistQuery.kt} (55%) rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnArtworksQuery.kt => queries/ArtworkQuery.kt} (52%) rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnAudiosFromQuery.kt => queries/AudioFromQuery.kt} (59%) create mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnGenresQuery.kt => queries/GenreQuery.kt} (58%) rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnPlaylistQuery.kt => queries/PlaylistQuery.kt} (57%) rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/OnWithFiltersQuery.kt => queries/WithFiltersQuery.kt} (60%) rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/{query/helper/OnAudioHelper.kt => queries/helper/QueryHelper.kt} (99%) delete mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAllPathQuery.kt delete mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosQuery.kt rename on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/{OnCursorProjections.kt => CursorProjection.kt} (100%) delete mode 100644 on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnDeviceInfo.kt create mode 100644 on_audio_query/ios/Classes/PluginProvider.swift create mode 100644 on_audio_query/ios/Classes/consts/Method.swift delete mode 100644 on_audio_query/ios/Classes/controller/OnAudioController.swift delete mode 100644 on_audio_query/ios/Classes/controller/OnPlaylistsController.swift create mode 100644 on_audio_query/ios/Classes/controllers/MethodController.swift create mode 100644 on_audio_query/ios/Classes/controllers/PermissionController.swift create mode 100644 on_audio_query/ios/Classes/controllers/PlaylistController.swift create mode 100644 on_audio_query/ios/Classes/queries/AlbumQuery.swift create mode 100644 on_audio_query/ios/Classes/queries/ArtistQuery.swift create mode 100644 on_audio_query/ios/Classes/queries/ArtworkQuery.swift rename on_audio_query/ios/Classes/{query/OnAudiosFromQuery.swift => queries/AudioFromQuery.swift} (63%) rename on_audio_query/ios/Classes/{query/OnAudioQuery.swift => queries/AudioQuery.swift} (51%) rename on_audio_query/ios/Classes/{query/OnGenresQuery.swift => queries/GenreQuery.swift} (55%) rename on_audio_query/ios/Classes/{query/OnPlaylistsQuery.swift => queries/PlaylistQuery.swift} (53%) rename on_audio_query/ios/Classes/{query/OnWithFiltersQuery.swift => queries/WithFiltersQuery.swift} (62%) rename on_audio_query/ios/Classes/{query => queries}/helper/OnAudioHelper.swift (80%) delete mode 100644 on_audio_query/ios/Classes/query/OnAlbumsQuery.swift delete mode 100644 on_audio_query/ios/Classes/query/OnArtistQuery.swift delete mode 100644 on_audio_query/ios/Classes/query/OnArtworkQuery.swift create mode 100644 on_audio_query/ios/Classes/utils/Log.swift delete mode 100644 on_audio_query/ios/Classes/utils/OnDeviceInfo.swift create mode 100644 on_audio_query_platform_interface/lib/details/log/log_config.dart create mode 100644 on_audio_query_platform_interface/lib/details/types/log_type.dart diff --git a/on_audio_query/CHANGELOG.md b/on_audio_query/CHANGELOG.md index a4ff38ff..19d50910 100644 --- a/on_audio_query/CHANGELOG.md +++ b/on_audio_query/CHANGELOG.md @@ -1,10 +1,47 @@ +## [[2.7.0](https://github.com/LucJosin/on_audio_query/releases/tag/2.7.0)] - [29.03.2023] + +### Features + +- **Added** `[LogType]`. +- **Added** `[LogConfig]`. +- **Added** `[PermissionController]` **(Native)** +- **Added** `[PluginProvider]` **(Native)** +- **Added** `[setLogConfig]` method. +- **Added** `[checkAndRequest]` method. +- **Added** `[controller]` to `[QueryArtworkWidget]`. +- **Added** `[retryRequest]` param to `[checkAndRequest]` and `[permissionsRequest]`. + +### Fixes + +#### Android + +- **Fixed** crash after request permission. - [#68](https://github.com/LucJosin/on_audio_query/issues/68) +- **Fixed** quality always being sent as `200` using `[queryArtwork]`. + +### Changes + +- **Updated** example. +- **Renamed** natives files/folders. +- **Reduced** the default `artwork` resolution (from 100 to 50). +- **Updated** `[QueryArtworkWidget]` params. +- **Updated** quality assert on `[QueryArtworkWidget]`. + +### ⚠ Important Changes + +- **Updated** application permission check. + - If application doesn't have permission to access the library, will throw a PlatformException. +- **Updated** `quality` param from `[QueryArtworkWidget]`. + - This param cannot be defined as null anymore and, by default, will be set to `50`. +- **Updated** minimum supported **Dart** version. + - Increase minimum version from `2.12` to `2.17`. + ## [2.6.2] - [03.03.2023] ### Fixes #### Android -- **[Fixed]** incompatibility with `Android 13`. - [#91](https://github.com/LucJosin/on_audio_query/issues/91) - Thanks [@ruchit-7span](https://github.com/ruchit-7span) +- **Fixed** incompatibility with `Android 13`. - [#91](https://github.com/LucJosin/on_audio_query/issues/91) - Thanks [@ruchit-7span](https://github.com/ruchit-7span) ## [2.6.1] - [05.17.2022] @@ -12,21 +49,21 @@ #### Android -- **[Fixed]** incompatibility with `Flutter 3`. - [#78](https://github.com/LucJosin/on_audio_query/issues/78) +- **Fixed** incompatibility with `Flutter 3`. - [#78](https://github.com/LucJosin/on_audio_query/issues/78) ## [2.6.0] - [02.01.2022] ### Features -- **[Added]** `[scanMedia]` method that will scan the given path and update the `[Android]` MediaStore. +- **Added** `[scanMedia]` method that will scan the given path and update the `[Android]` MediaStore. ### Fixes -- **[Fixed]** media showing when calling `[querySongs]` even after deleting with `[dart:io]`. - [#67](https://github.com/LucJosin/on_audio_query/issues/67) +- **Fixed** media showing when calling `[querySongs]` even after deleting with `[dart:io]`. - [#67](https://github.com/LucJosin/on_audio_query/issues/67) ### Changes -- **[Updated]** some required packages. +- **Updated** some required packages. ### Documentation @@ -39,7 +76,7 @@ ### Changes -- **[Updated]** all Github links. +- **Updated** all Github links. ## [2.5.3] - [11.10.2021] @@ -47,8 +84,8 @@ #### IOS -- **[Fixed]** song/artist/album from `Apple Music` returning when 'querying' - [#61](https://github.com/LucJosin/on_audio_query/issues/61) -- **[Fixed]** wrong `artistId` returning from `[AlbumModel]` - [#60](https://github.com/LucJosin/on_audio_query/issues/60) +- **Fixed** song/artist/album from `Apple Music` returning when 'querying' - [#61](https://github.com/LucJosin/on_audio_query/issues/61) +- **Fixed** wrong `artistId` returning from `[AlbumModel]` - [#60](https://github.com/LucJosin/on_audio_query/issues/60) ### Documentation @@ -60,7 +97,7 @@ #### Android -- **[Fixed]** wrong value returning from: - [#56](https://github.com/LucJosin/on_audio_query/issues/56) +- **Fixed** wrong value returning from: - [#56](https://github.com/LucJosin/on_audio_query/issues/56) - `[is_music]`. - `[is_alarm]`. - `[is_notification]`. @@ -78,11 +115,11 @@ #### Dart -- **[Fixed]** wrong value returning from `[artistId]` when using `[AlbumModel]`. - [#54](https://github.com/LucJosin/on_audio_query/issues/54) +- **Fixed** wrong value returning from `[artistId]` when using `[AlbumModel]`. - [#54](https://github.com/LucJosin/on_audio_query/issues/54) #### Android -- **[Fixed]** missing songs from `[queryAudiosFrom]` when using `GENRE`. - [#46](https://github.com/LucJosin/on_audio_query/issues/46) +- **Fixed** missing songs from `[queryAudiosFrom]` when using `GENRE`. - [#46](https://github.com/LucJosin/on_audio_query/issues/46) ### Documentation @@ -104,14 +141,14 @@ #### Dart -- **[Added]** `errorBuilder` and `frameBuilder` to `[QueryArtworkWidget]`. +- **Added** `errorBuilder` and `frameBuilder` to `[QueryArtworkWidget]`. ### Fixes #### Web -- **[Fixed]** empty result when using `[querySongs]`. -- **[Fixed]** error when decoding some images. +- **Fixed** empty result when using `[querySongs]`. +- **Fixed** error when decoding some images. See all development [changes](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md): @@ -123,23 +160,23 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### All platforms -- **[Added]** `artwork` to genres. - [#41](https://github.com/LucJosin/on_audio_query/issues/41) -- **[Added]** `sortType`, `orderType` and `ignoreCase` to `[queryAudiosFrom]`. +- **Added** `artwork` to genres. - [#41](https://github.com/LucJosin/on_audio_query/issues/41) +- **Added** `sortType`, `orderType` and `ignoreCase` to `[queryAudiosFrom]`. #### Android -- Re-**[Added]** `path` parameter to `[querySongs]`. - [#48](https://github.com/LucJosin/on_audio_query/issues/48) +- Re-**Added** `path` parameter to `[querySongs]`. - [#48](https://github.com/LucJosin/on_audio_query/issues/48) #### Web -- **[Added]** `path` parameter to `[querySongs]`. +- **Added** `path` parameter to `[querySongs]`. ### Fixes #### Android -- **[Fixed]** empty `Uint8List` when using `[queryArtwork]` on Android 7. - [#47](https://github.com/LucJosin/on_audio_query/issues/47) -- **[Fixed]** null `albumId` when using Android 9 or below. - [#53](https://github.com/LucJosin/on_audio_query/issues/53) +- **Fixed** empty `Uint8List` when using `[queryArtwork]` on Android 7. - [#47](https://github.com/LucJosin/on_audio_query/issues/47) +- **Fixed** null `albumId` when using Android 9 or below. - [#53](https://github.com/LucJosin/on_audio_query/issues/53) ### Documentation @@ -163,7 +200,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### IOS -- **[Fixed]** no artwork returning from `[queryArtwork]` when using `ArtworkType.ALBUM`. - [#45](https://github.com/LucJosin/on_audio_query/issues/45) +- **Fixed** no artwork returning from `[queryArtwork]` when using `ArtworkType.ALBUM`. - [#45](https://github.com/LucJosin/on_audio_query/issues/45) ### Documentation @@ -175,13 +212,13 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Dart -- **[Fixed]** wrong type of `numOfSongs` from `[SongModel]`. - [#39](https://github.com/LucJosin/on_audio_query/issues/39) +- **Fixed** wrong type of `numOfSongs` from `[SongModel]`. - [#39](https://github.com/LucJosin/on_audio_query/issues/39) #### IOS -- **[Fixed]** wrong filter configuration when using `[queryWithFilters]`. -- **[Fixed]** crash when using any `'query'` method with a null `sortType`. - [#43](https://github.com/LucJosin/on_audio_query/issues/43) -- **[Fixed]** error with wrong `[MPMediaQuery]` filter. - [#38](https://github.com/LucJosin/on_audio_query/issues/38) +- **Fixed** wrong filter configuration when using `[queryWithFilters]`. +- **Fixed** crash when using any `'query'` method with a null `sortType`. - [#43](https://github.com/LucJosin/on_audio_query/issues/43) +- **Fixed** error with wrong `[MPMediaQuery]` filter. - [#38](https://github.com/LucJosin/on_audio_query/issues/38) ### Documentation @@ -193,13 +230,13 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Added]** a better 'search' method to `[queryWithFilters]`, now the query uses 'contains' when 'querying'. - [#35](https://github.com/LucJosin/on_audio_query/issues/35) +- **Added** a better 'search' method to `[queryWithFilters]`, now the query uses 'contains' when 'querying'. - [#35](https://github.com/LucJosin/on_audio_query/issues/35) ### Fixes #### IOS -- **[Fixed]** error with wrong `[MPMediaQuery]` type and wrong value from `[jpegData]`. - [#37](https://github.com/LucJosin/on_audio_query/issues/37) +- **Fixed** error with wrong `[MPMediaQuery]` type and wrong value from `[jpegData]`. - [#37](https://github.com/LucJosin/on_audio_query/issues/37) #### Documentation @@ -215,7 +252,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android/Web -- **[Added]** `[ignoreCase]` to: +- **Added** `[ignoreCase]` to: - `[querySongs]`. - `[queryAlbums]`. - `[queryArtists]`. @@ -226,9 +263,9 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `error` when trying to build using `Android`. - [#32](https://github.com/LucJosin/on_audio_query/issues/32) & [#33](https://github.com/LucJosin/on_audio_query/issues/33) -- **[Fixed]** `error` related to android song projection. - [#31](https://github.com/LucJosin/on_audio_query/issues/31) -- **[Fixed]** `'bug'` when using `SongSortType.TITLE`. This is now a `'feature'` and can be controlled using `[ignoreCase]`. - [#29](https://github.com/LucJosin/on_audio_query/issues/29) +- **Fixed** `error` when trying to build using `Android`. - [#32](https://github.com/LucJosin/on_audio_query/issues/32) & [#33](https://github.com/LucJosin/on_audio_query/issues/33) +- **Fixed** `error` related to android song projection. - [#31](https://github.com/LucJosin/on_audio_query/issues/31) +- **Fixed** `'bug'` when using `SongSortType.TITLE`. This is now a `'feature'` and can be controlled using `[ignoreCase]`. - [#29](https://github.com/LucJosin/on_audio_query/issues/29) ### Documentation @@ -247,17 +284,17 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android/IOS/Web -- **[Added]** `[numOfSongs]` to `[PlaylistModel]` and `[GenreModel]`. -- **[Added]** `Playlist` and `Artist` to `ArtworkType`. +- **Added** `[numOfSongs]` to `[PlaylistModel]` and `[GenreModel]`. +- **Added** `Playlist` and `Artist` to `ArtworkType`. #### Android/IOS -- **[Added]** `quality` to `queryArtwork`. +- **Added** `quality` to `queryArtwork`. #### Android -- **[Added]** `[isAudioBook]`, `[Genre]` and `[GenreId]` to `[SongModel]`. -- Re-**[Added]** to `[SongModel]`: +- **Added** `[isAudioBook]`, `[Genre]` and `[GenreId]` to `[SongModel]`. +- Re-**Added** to `[SongModel]`: - `[isAlarm]`. - `[isMusic]`. - `[isNotification]`. @@ -268,9 +305,9 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** wrong value returning from `[id]` when using `[ArtistModel]`. -- **[Fixed]** wrong value returning from `[id]` when using `[GenreModel]`. -- **[Fixed]** no value returning from `[queryAudiosFrom]` when using `ARTIST_ID`. +- **Fixed** wrong value returning from `[id]` when using `[ArtistModel]`. +- **Fixed** wrong value returning from `[id]` when using `[GenreModel]`. +- **Fixed** no value returning from `[queryAudiosFrom]` when using `ARTIST_ID`. ### Documentation @@ -316,7 +353,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### IOS -- **[Fixed]** wrong value returning from `[permissionsStatus]`. - [#24](https://github.com/LucJosin/on_audio_query/issues/24) +- **Fixed** wrong value returning from `[permissionsStatus]`. - [#24](https://github.com/LucJosin/on_audio_query/issues/24) ### Documentation @@ -328,9 +365,9 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** duplicate `media` from `[queryWithFilters]`. -- **[Fixed]** `crash` when calling `[queryWithFilters]`. - [#23](https://github.com/LucJosin/on_audio_query/issues/23) -- **[Fixed]** `null` artwork returning from `[queryArtwork]` on Android 11/R. - [#21](https://github.com/LucJosin/on_audio_query/issues/21) +- **Fixed** duplicate `media` from `[queryWithFilters]`. +- **Fixed** `crash` when calling `[queryWithFilters]`. - [#23](https://github.com/LucJosin/on_audio_query/issues/23) +- **Fixed** `null` artwork returning from `[queryArtwork]` on Android 11/R. - [#21](https://github.com/LucJosin/on_audio_query/issues/21) ### Documentation @@ -343,7 +380,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** error when using `[removeFromPlaylist]`. - [#22](https://github.com/LucJosin/on_audio_query/issues/22) +- **Fixed** error when using `[removeFromPlaylist]`. - [#22](https://github.com/LucJosin/on_audio_query/issues/22) ### Documentation @@ -407,7 +444,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** no value returning when using `[permissionsRequest]`. +- **Fixed** no value returning when using `[permissionsRequest]`. ### Documentation @@ -431,8 +468,8 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### IOS -- **[Fixed]** crash when using `[queryArtwork]`. -- **[Fixed]** wrong `[id]` value returning from `[PlaylistModel]`. +- **Fixed** crash when using `[queryArtwork]`. +- **Fixed** wrong `[id]` value returning from `[PlaylistModel]`. ### Documentation @@ -450,11 +487,11 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `error` when building to `[Android]`. +- **Fixed** `error` when building to `[Android]`. #### IOS -- **[Fixed]** wrong `[duration]`, `[dateAdded]` and `[bookmark]` values returning from `[SongModel]`. +- **Fixed** wrong `[duration]`, `[dateAdded]` and `[bookmark]` values returning from `[SongModel]`. ### Documentation @@ -655,7 +692,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `[Kotlin]` issue when installing the plugin. +- **Fixed** `[Kotlin]` issue when installing the plugin. ### Documentation @@ -673,7 +710,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`. - [#16](https://github.com/LucJosin/on_audio_query/issues/16) and [#12](https://github.com/LucJosin/on_audio_query/issues/12) +- **Fixed** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`. - [#16](https://github.com/LucJosin/on_audio_query/issues/16) and [#12](https://github.com/LucJosin/on_audio_query/issues/12) ### Documentation @@ -691,7 +728,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- ~~**[Fixed]** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`.~~ +- ~~**Fixed** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`.~~ ### Documentation @@ -709,7 +746,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `java.lang.Integer cannot be cast to java.lang.Long` from `[queryArtworks]`. - [#11](https://github.com/LucJosin/on_audio_query/issues/11) +- **Fixed** `java.lang.Integer cannot be cast to java.lang.Long` from `[queryArtworks]`. - [#11](https://github.com/LucJosin/on_audio_query/issues/11) ### Documentation @@ -742,11 +779,11 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** incompatibility with `[permission_handler]`. - [#3](https://github.com/LucJosin/on_audio_query/issues/3) - Thanks [@mvanbeusekom](https://github.com/mvanbeusekom) +- **Fixed** incompatibility with `[permission_handler]`. - [#3](https://github.com/LucJosin/on_audio_query/issues/3) - Thanks [@mvanbeusekom](https://github.com/mvanbeusekom) #### Dart -- **[Fixed]** wrong name. From `[dataAdded]` to `[dateAdded]`. +- **Fixed** wrong name. From `[dataAdded]` to `[dateAdded]`. ### Documentation @@ -781,7 +818,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** Now `[queryArtworks]` will return null. - [#6](https://github.com/LucJosin/on_audio_query/issues/6) +- **Fixed** Now `[queryArtworks]` will return null. - [#6](https://github.com/LucJosin/on_audio_query/issues/6) ### Documentation @@ -835,7 +872,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Android -- **[Fixed]** `[queryArtwork]` returning null album image in Android 11. - [#1](https://github.com/LucJosin/on_audio_query/issues/1) +- **Fixed** `[queryArtwork]` returning null album image in Android 11. - [#1](https://github.com/LucJosin/on_audio_query/issues/1) ### Documentation @@ -888,12 +925,12 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma #### Dart -- **[Fixed]** flutter example. +- **Fixed** flutter example. #### Android -- **[Fixed]** `[audiosFromPlaylist]` [**Now this method is part of queryAudiosFrom**] -- **[Fixed]** `"count(*)"` error from `[addToPlaylist]`. [**Permission bug on Android 10 still happening**] +- **Fixed** `[audiosFromPlaylist]` [**Now this method is part of queryAudiosFrom**] +- **Fixed** `"count(*)"` error from `[addToPlaylist]`. [**Permission bug on Android 10 still happening**] ### Documentation @@ -1063,6 +1100,9 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma ### Changes - TODO +### Refactor +- TODO + ### ⚠ Important Changes #### @**Deprecated** - TODO @@ -1070,7 +1110,7 @@ See all development [changes](https://github.com/LucJosin/on_audio_query/blob/ma diff --git a/on_audio_query/README.md b/on_audio_query/README.md index c5163bef..a98e8dee 100644 --- a/on_audio_query/README.md +++ b/on_audio_query/README.md @@ -1,3 +1,5 @@ +
+ # on_audio_query [![Pub.dev](https://img.shields.io/pub/v/on_audio_query?color=9cf&label=Pub.dev&style=flat-square)](https://pub.dev/packages/on_audio_query) [![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20IOS%20%7C%20Web-9cf?&style=flat-square)]() @@ -5,28 +7,16 @@ `on_audio_query` is a [Flutter](https://flutter.dev/) Plugin used to query audios/songs 🎶 infos [title, artist, album, etc..] from device storage.
-## Help: - **Any problem? [Issues](https://github.com/LucJosin/on_audio_query/issues)**
**Any suggestion? [Pull request](https://github.com/LucJosin/on_audio_query/pulls)** -### Extensions: - -* [on_audio_edit](https://github.com/LucJosin/on_audio_edit) - Used to edit audio metadata. -* [on_audio_room](https://github.com/LucJosin/on_audio_room) - Used to store audio [Favorites, Most Played, etc..]. - -### Translations: - -NOTE: Feel free to help with readme translations - -* [English](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/README.md) -* [Portuguese](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/README.pt-BR.md) +
### Topics: -* [How to Install](#how-to-install) +* [Installation](#installation) * [Platforms](#platforms) -* [How to use](#how-to-use) +* [Overview](#overview) * [Examples](#examples) * [Gif Examples](#gif-examples) * [License](#license) @@ -60,30 +50,44 @@ NOTE: Feel free to help with readme translations **[See all platforms methods support](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/PLATFORMS.md)** -## How to Install: +## Installation: + Add the following code to your `pubspec.yaml`: ```yaml dependencies: - on_audio_query: ^2.6.0 + on_audio_query: ^2.7.0 ``` ### Request Permission: + #### Android: -To use this plugin add the following code to your `AndroidManifest.xml` +To use this plugin add the following code to your [AndroidManifest.xml](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/example/android/app/src/main/AndroidManifest.xml) ```xml - ... - - + + + + + + + + + ``` #### IOS: -To use this plugin add the following code to your `Info.plist` -``` +To use this plugin add the following code to your [Info.plist](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/example/ios/Runner/Info.plist) +```plist + + ... + NSAppleMusicUsageDescription - ..Add a reason.. + $(PROJECT_NAME) requires access to media library + + ... + ``` #### Web: @@ -113,63 +117,16 @@ Since Web Browsers **don't** offer direct access to their user's `file system`, * Add/Remove/Move specific audios to playlists. * Specific sort types for all query methods. -## TODO: +## Overview: -* Add better performance for all plugin. -* Add support to Windows/MacOs/Linux. -* Option to remove songs. -* Fix bugs. - -## How to use: - -```dart -OnAudioQuery() // The main method to start using the plugin. -``` All types of methods on this plugin: -### Query methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`querySongs`](#querysongs) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryAlbums`](#queryalbums) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryArtists`](#queryartists) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryPlaylists`](#queryplaylists) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryGenres`](#querygenres) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryAudiosFrom`](#queryaudiosfrom) | `(Type, Where, RequestPermission)` | `List` |
-| [`queryWithFilters`](#querywithfilters) | `(ArgsVal, WithFiltersType, Args, RequestPermission)` | `List` |
-| [`queryArtwork`](#queryArtwork) | `(Id, Type, Format, Size, RequestPermission)` | `Uint8List?` |
- -### Playlist methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`createPlaylist`]() | `(PlaylistName, RequestPermission)` | `bool` |
-| [`removePlaylist`]() | `(PlaylistId, RequestPermission)` | `bool` |
-| [`addToPlaylist`]() | **[BG]**`(PlaylistId, AudioId, RequestPermission)` | `bool` |
-| [`removeFromPlaylist`]() | `(PlaylistId, AudioId, RequestPermission)` | `bool` |
-| [`renamePlaylist`]() | `(PlaylistId, NewName, RequestPermission)` | `bool` |
-| [`moveItemTo`]() | **[NT]**`(PlaylistId, From, To, RequestPermission)` | `bool` |
- -### Permissions/Device methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`permissionsRequest`]() | `(retryRequest)` | `bool` |
-| [`permissionsStatus`]() | | `bool` |
-| [`queryDeviceInfo`]() | | `DeviceModel` |
- -### Others methods -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`scanMedia`](#scanmedia) | `(Path)` | `bool` |
- ### Artwork Widget ```dart Widget someOtherName() async { return QueryArtworkWidget( - id: SongId, + id: , type: ArtworkType.AUDIO, ); } @@ -177,72 +134,37 @@ All types of methods on this plugin: **See more: [QueryArtworkWidget](https://pub.dev/documentation/on_audio_query/latest/on_audio_query/QueryArtworkWidget-class.html)** -### Abbreviations - -**[NT]** -> Need Tests
-**[BG]** -> Bug on Android 10/Q - ## Examples: #### OnAudioQuery -```dart - final OnAudioQuery _audioQuery = OnAudioQuery(); -``` -#### querySongs ```dart - someName() async { - // DEFAULT: - // SongSortType.TITLE, - // OrderType.ASC_OR_SMALLER, - // UriType.EXTERNAL, - List something = await _audioQuery.querySongs(); - } +final OnAudioQuery _audioQuery = OnAudioQuery(); ``` -#### queryAlbums -```dart - someName() async { - // DEFAULT: - // AlbumSortType.ALBUM, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryAlbums(); - } -``` +#### Query methods: -#### queryArtists -```dart - someName() async { - // DEFAULT: - // ArtistSortType.ARTIST, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryArtists(); - } -``` +- queryAudios(); +- queryAlbums(); +- queryArtists(); +- queryPlaylists(); +- queryGenres(). -#### queryPlaylists ```dart someName() async { - // DEFAULT: - // PlaylistSortType.NAME, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryPlaylists(); - } -``` + // Query Audios + List audios = await _audioQuery.queryAudios(); -#### queryGenres -```dart - someName() async { - // DEFAULT: - // GenreSortType.NAME, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryGenres(); + // Query Albums + List albums = await _audioQuery.queryAlbums(); } ``` #### scanMedia + You'll use this method when updating a media from storage. This method will update the media 'state' and Android `MediaStore` will be able to know this 'state'. + ```dart someName() async { OnAudioQuery _audioQuery = OnAudioQuery(); @@ -259,12 +181,13 @@ Android `MediaStore` will be able to know this 'state'. ``` #### queryArtwork + ```dart someName() async { // DEFAULT: ArtworkFormat.JPEG, 200 and false Uint8List something = await _audioQuery.queryArtwork( - SongId, - ArtworkType.AUDIO, + , + ArtworkType.AUDIO, ..., ); } @@ -273,71 +196,6 @@ Android `MediaStore` will be able to know this 'state'. Or you can use a basic and custom Widget. **See example [QueryArtworkWidget](#artwork-widget)** -#### queryAudiosFrom -You can use this method to 'query' the songs from any section(Album, Artist, Playlist or Genre). -```dart - someName() async { - List something = await _audioQuery.queryAudiosFrom( - AudiosFromType.ALBUM_ID, - albumId, - // You can also define a sortType - sortType: SongSortType.TITLE, // Default - orderType: OrderType.ASC_OR_SMALLER, // Default - ); - } -``` - -#### queryWithFilters -```dart - someName() async { - // Here we'll search for a [song](WithFiltersType.AUDIOS) using his - // [artist](AudiosArgs.ARTIST) - List something = await _audioQuery.queryWithFilters( - // The [text] to search - "Sam Smith", - // The type of search you want. - // All types: - // * WithFiltersType.AUDIOS - // * WithFiltersType.ALBUMS - // * WithFiltersType.PLAYLISTS - // * WithFiltersType.ARTISTS - // * WithFiltersType.GENRES - WithFiltersType.AUDIOS, - // This method has [args] as parameter. With this value you can create - // a more 'advanced' search. - args: AudiosArgs.ARTIST, - ); - - // Other example: - - // Here we'll search for a [song](WithFiltersType.AUDIOS) using his - // [album](AudiosArgs.ALBUM) - List something = await _audioQuery.queryWithFilters( - // The [text] to search - "In the Lonely Hour", - // The type of search you want. - // All types: - // * WithFiltersType.AUDIOS - // * WithFiltersType.ALBUMS - // * WithFiltersType.PLAYLISTS - // * WithFiltersType.ARTISTS - // * WithFiltersType.GENRES - WithFiltersType.AUDIOS, - // This method has [args] as parameter. With this value you can create - // a more 'advanced' search. - args: AudiosArgs.ALBUM, - ); - - // After getting the result from [queryWithFilters], convert this list using: - List convertedList = something.toTypeModel(); - - // Example: - List convertedSongs = something.toSongModel(); - } -``` - -ArgsTypes: [AudiosArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/AudiosArgs-class.html), [AlbumsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/AlbumsArgs-class.html), [PlaylistsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/PlaylistsArgs-class.html), [ArtistsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/ArtistsArgs-class.html) and [GenresArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/GenresArgs-class.html) - ## Gif Examples: | | | | | |:---:|:---:|:---:|:---:| diff --git a/on_audio_query/README.pt-BR.md b/on_audio_query/README.pt-BR.md deleted file mode 100644 index 68feb322..00000000 --- a/on_audio_query/README.pt-BR.md +++ /dev/null @@ -1,352 +0,0 @@ -# on_audio_query -[![Pub.dev](https://img.shields.io/pub/v/on_audio_query?color=9cf&label=Pub.dev&style=flat-square)](https://pub.dev/packages/on_audio_query) -[![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20IOS%20%7C%20Web-9cf?&style=flat-square)]() -[![Languages](https://img.shields.io/badge/Language-Dart%20%7C%20Kotlin%20%7C%20Swift-9cf?&style=flat-square)]() - -`on_audio_query` é um [Flutter](https://flutter.dev/) Plugin usado para adquirir informações de áudios/músicas 🎶 [título, artista, album, etc..] do celular.
- -## Ajuda: - -**Algum problema? [Issues](https://github.com/LucJosin/on_audio_query/issues)**
-**Alguma sugestão? [Pull request](https://github.com/LucJosin/on_audio_query/pulls)** - -### Extensões: - -* [on_audio_edit](https://github.com/LucJosin/on_audio_edit) - Usado para editar audio metadata. -* [on_audio_room](https://github.com/LucJosin/on_audio_room) - Usado para guardar audio [Favoritos, Mais tocados, etc..]. - -### Traduções: - -NOTE: Fique à vontade para ajudar nas traduções - -* [Inglês](README.md) -* [Português](README.pt-BR.md) - -## Tópicos: - -* [Como instalar](#como-instalar) -* [Plataformas](#platformas) -* [Como usar](#como-usar) -* [Exemplos](#exemplos) -* [Exemplos em Gif](#exemplos-em-gif) -* [Licença](#licença) - -## Platformas: - - -| Methods | Android | IOS | Web | -|-------|:----------:|:----------:|:----------:| -| `querySongs` | `✔️` | `✔️` | `✔️` |
-| `queryAlbums` | `✔️` | `✔️` | `✔️` |
-| `queryArtists` | `✔️` | `✔️` | `✔️` |
-| `queryPlaylists` | `✔️` | `✔️` | `❌` |
-| `queryGenres` | `✔️` | `✔️` | `✔️` |
-| `queryAudiosFrom` | `✔️` | `✔️` | `✔️` |
-| `queryWithFilters` | `✔️` | `✔️` | `✔️` |
-| `queryArtwork` | `✔️` | `✔️` | `✔️` |
-| `createPlaylist` | `✔️` | `✔️` | `❌` |
-| `removePlaylist` | `✔️` | `❌` | `❌` |
-| `addToPlaylist` | `✔️` | `✔️` | `❌` |
-| `removeFromPlaylist` | `✔️` | `❌` | `❌` |
-| `renamePlaylist` | `✔️` | `❌` | `❌` |
-| `moveItemTo` | `✔️` | `❌` | `❌` |
-| `permissionsRequest` | `✔️` | `✔️` | `❌` |
-| `permissionsStatus` | `✔️` | `✔️` | `❌` |
-| `queryDeviceInfo` | `✔️` | `✔️` | `✔️` |
-| `scanMedia` | `✔️` | `❌` | `❌` |
- -✔️ -> Tem suporte
-❌ -> Não tem suporte
- -**[Veja todos os suportes](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/PLATFORMS.md)** - -## Como instalar: -Adicione o seguinte codigo para seu `pubspec.yaml`: -```yaml -dependencies: - on_audio_query: ^2.6.0 -``` - -#### Solicitar Permissões: -#### Android: -Para usar esse plugin adicione o seguinte código no seu `AndroidManifest.xml` -```xml - ... - - - - - -``` - -#### IOS: -Para usar esse plugin adicione o seguinte código no seu `Info.plist` -```plist - NSAppleMusicUsageDescription - ..Adicione um motivo.. -``` - -#### Web: -Já que os navegadores **não** oferecem acesso direto ao `file system` dos usuários, esse plugin irá usar a pasta `assets` para "pegar" os audios. Então, dependerá totalmente do `desenvolvedor`. - -```yaml - # Você não precisa adicionar todos os audios, apenas defina a pasta. - assets: - - assets/ - # Se seus arquivos estão em outra pasta dentro de `assets`: - - assets/audios/ - # - assets/audios/animals/ - # - assets/audios/animals/cat/ - # ... -``` - -## Algumas qualidades: - -* Opcional e Interna solicitação de permissão para `LER` e `ESCREVER`. -* Pega todos os áudios. -* Pega todos os albums e áudios específicos dos albums. -* Pega todos os artistas e áudios específicos dos artistas. -* Pega todas as playlists e áudios específicos das playlists. -* Pega todos os gêneros e áudios específicos dos gêneros. -* Pega todos os métodos de query com `keys` específicas [Search/Busca]. -* Pega todos as pastas e áudios específicos das pastas. -* Criar/Deletar/Renomear playlists. -* Adicionar/Remover/Mover específicos áudios para playlists. -* Específicos tipos de classificação para todos os métodos. - -## Para fazer: - -* Adicionar uma melhor performace para todo o plugin. -* Adicionar suporte para Windows/MacOs/Linux. -* Opção para remover músicas. -* Arrumar erros. - -## Como usar: - -```dart -OnAudioQuery() // O comando principal para usar o plugin. -``` -Todos os tipos de métodos nesse plugin: - -### Query methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`querySongs`](#querysongs) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryAlbums`](#queryalbums) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryArtists`](#queryartists) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryPlaylists`](#queryplaylists) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryGenres`](#querygenres) | `(SortType, OrderType, UriType, RequestPermission)` | `List` |
-| [`queryAudiosFrom`](#queryaudiosfrom) | `(Type, Where, RequestPermission)` | `List` |
-| [`queryWithFilters`](#queryWithFilters) | `(ArgsVal, WithFiltersType, Args, RequestPermission)` | `List` |
-| [`queryArtwork`](#queryArtwork) | `(Id, Type, Format, Size, RequestPermission)` | `Uint8List?` |
- -### Playlist methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`createPlaylist`]() | `(PlaylistName, RequestPermission)` | `bool` |
-| [`removePlaylist`]() | `(PlaylistId, RequestPermission)` | `bool` |
-| [`addToPlaylist`]() | **[BG]**`(PlaylistId, AudioId, RequestPermission)` | `bool` |
-| [`removeFromPlaylist`]() | `(PlaylistId, AudioId, RequestPermission)` | `bool` |
-| [`renamePlaylist`]() | `(PlaylistId, NewName, RequestPermission)` | `bool` |
-| [`moveItemTo`]() | **[NT]**`(PlaylistId, From, To, RequestPermission)` | `bool` |
- -### Permissions/Device methods - -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`permissionsRequest`]() | `(retryRequest)` | `bool` |
-| [`permissionsStatus`]() | | `bool` |
-| [`queryDeviceInfo`]() | | `DeviceModel` |
- -### Others methods -| Methods | Parameters | Return | -|--------------|-----------------|-----------------| -| [`scanMedia`](#scanmedia) | `(Path)` | `bool` |
- -### Artwork Widget - -```dart - Widget someOtherName() async { - return QueryArtworkWidget( - id: SongId, - type: ArtworkType.AUDIO, - ); - } -``` - -**Veja mais: QueryArtworkWidget** - -### Abreviações - -**[NT]** -> Precisa de testes
-**[BG]** -> Bug no Android 10/Q - -## Exemplos: - -#### OnAudioQuery -```dart - final OnAudioQuery _audioQuery = OnAudioQuery(); -``` - -#### querySongs -```dart - someName() async { - // DEFAULT: - // SongSortType.TITLE, - // OrderType.ASC_OR_SMALLER, - // UriType.EXTERNAL, - List something = await _audioQuery.querySongs(); - } -``` - -#### queryAlbums -```dart - someName() async { - // DEFAULT: - // AlbumSortType.ALBUM, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryAlbums(); - } -``` - -#### queryArtists -```dart - someName() async { - // DEFAULT: - // ArtistSortType.ARTIST, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryArtists(); - } -``` - -#### queryPlaylists -```dart - someName() async { - // DEFAULT: - // PlaylistSortType.NAME, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryPlaylists(); - } -``` - -#### queryGenres -```dart - someName() async { - // DEFAULT: - // GenreSortType.NAME, - // OrderType.ASC_OR_SMALLER - List something = await _audioQuery.queryGenres(); - } -``` - -#### scanMedia -Você irá usar esse método quando atualizar uma media do armazenamento. Esse método irá atualizar o 'estado' da mídia e -o Android `MediaStore` irá saber esse 'estado'. -```dart - someName() async { - OnAudioQuery _audioQuery = OnAudioQuery(); - File file = File('path'); - try { - if (file.existsSync()) { - file.deleteSync(); - _audioQuery.scanMedia(file.path); // Atualiza o 'caminho' da mídia - } - } catch (e) { - debugPrint('$e'); - } - } -``` - -#### queryArtwork -```dart - someName() async { - // DEFAULT: ArtworkFormat.JPEG, 200 and false - Uint8List something = await _audioQuery.queryArtwork( - SongId, - ArtworkType.AUDIO, - ..., - ); - } -``` - -Ou você pode usar um Widget básico e customizável. -**Veja o exemplo [QueryArtworkWidget](#artwork-widget)** - -#### queryAudiosFrom -Você pode usar esse método para 'pegar' as músicas de qualquer seção(Album, Artista, Playlist or Gênero). -```dart - someName() async { - List something = await _audioQuery.queryAudiosFrom( - AudiosFromType.ALBUM_ID, - albumId, - // Você pode também definir um tipo de classificação. - sortType: SongSortType.TITLE, // Default - orderType: OrderType.ASC_OR_SMALLER, // Default - ); - } -``` - -#### queryWithFilters -```dart - someName() async { - // Aqui nós iremos pesquisar por uma [música](WithFiltersType.AUDIOS) usando o seu - // [artista](AudiosArgs.ARTIST) - List something = await _audioQuery.queryWithFilters( - // O [texto] para pesquisar - "Sam Smith", - // O tipo de pesquisa que você quer. - // Todos os tipos: - // * WithFiltersType.AUDIOS - // * WithFiltersType.ALBUMS - // * WithFiltersType.PLAYLISTS - // * WithFiltersType.ARTISTS - // * WithFiltersType.GENRES - WithFiltersType.AUDIOS, - // Este método possui [args] como parâmetro. Com este valor você pode criar - // uma pesquisa mais 'avançada'. - args: AudiosArgs.ARTIST, - ); - - // Outro exemplo: - - // Aqui nós iremos pesquisar por uma [música](WithFiltersType.AUDIOS) usando o seu - // [album](AudiosArgs.ALBUM) - List something = await _audioQuery.queryWithFilters( - // O [texto] para pesquisar - "In the Lonely Hour", - // O tipo de pesquisa que você quer. - // Todos os tipos: - // * WithFiltersType.AUDIOS - // * WithFiltersType.ALBUMS - // * WithFiltersType.PLAYLISTS - // * WithFiltersType.ARTISTS - // * WithFiltersType.GENRES - WithFiltersType.AUDIOS, - // Este método possui [args] como parâmetro. Com este valor você pode criar - // uma pesquisa mais 'avançada'. - args: AudiosArgs.ALBUM, - ); - - // Depois de adquirir o resultado do [queryWithFilters], converta para uma lista usando: - List convertedList = something.toTypeModel(); - - // Exemplo: - List convertedSongs = something.toSongModel(); - } -``` - -ArgsTypes: [AudiosArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/AudiosArgs-class.html), [AlbumsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/AlbumsArgs-class.html), [PlaylistsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/PlaylistsArgs-class.html), [ArtistsArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/ArtistsArgs-class.html) and [GenresArgs](https://pub.dev/documentation/on_audio_query_platform_interface/latest/on_audio_query_helper/GenresArgs-class.html) - -## Exemplos em Gif: -| | | | | -|:---:|:---:|:---:|:---:| -| | | | | -| Músicas | Albums | Playlists | Artistas | - -## LICENÇA: - -* [LICENSE](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE) - -> * [Voltar ao Topo](#on_audio_query) diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt index 4b9f69df..93d4d755 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt @@ -14,19 +14,12 @@ Copyright: © 2021, Lucas Josino. All rights reserved. package com.lucasjosino.on_audio_query -import android.Manifest -import android.app.Activity -import android.content.Context -import android.content.pm.PackageManager import android.media.MediaScannerConnection import android.os.Build -import androidx.annotation.NonNull -import androidx.annotation.RequiresApi -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.lucasjosino.on_audio_query.controller.OnAudioController -import com.lucasjosino.on_audio_query.utils.queryDeviceInfo -import com.lucasjosino.on_audio_query.interfaces.OnPermissionManagerInterface +import com.lucasjosino.on_audio_query.consts.Method +import com.lucasjosino.on_audio_query.controllers.MethodController +import com.lucasjosino.on_audio_query.controllers.PermissionController +import io.flutter.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -34,184 +27,157 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry -/** OnAudioQueryPlugin Central */ -class OnAudioQueryPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, - OnPermissionManagerInterface, PluginRegistry.RequestPermissionsResultListener { +class OnAudioQueryPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + + init { + // Set default logging level + Log.setLogLevel(Log.WARN) + } + + companion object { + // Get the current class name. + private const val TAG: String = "OnAudioQueryPlugin" + + // Method channel name. + private const val CHANNEL_NAME = "com.lucasjosino.on_audio_query" + } + + private var permissionController = PermissionController() + private var methodController = MethodController() + + private var binding: ActivityPluginBinding? = null - // Dart <-> Kotlin communication - private val channelName = "com.lucasjosino.on_audio_query" private lateinit var channel: MethodChannel - // Main parameters - private var retryRequest: Boolean = false - private lateinit var pContext: Context - private lateinit var pActivity: Activity - private lateinit var pResult: Result - private lateinit var onAudioController: OnAudioController - - // - private val onPermission = arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - ) - - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - private val tiramisuPermission = arrayOf( - Manifest.permission.READ_MEDIA_AUDIO, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO - ) - - // This is only important for initialization - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - this.pContext = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, channelName) + // Dart <-> Kotlin communication + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + Log.i(TAG, "Attached to engine") + + // Setup the method channel communication. + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) channel.setMethodCallHandler(this) } // Methods will always follow the same route: // Receive method -> check permission -> controller -> do what's needed -> return to dart - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - pResult = result; onAudioController = OnAudioController(pContext, call, result) + override fun onMethodCall(call: MethodCall, result: Result) { + Log.d(TAG, "Started method call (${call.method})") + + // Init the plugin provider with current 'call' and 'result'. + PluginProvider.setCurrentMethod(call, result) // If user deny permission request a pop up will immediately show up // If [retryRequest] is null, the message will only show when call method again - retryRequest = call.argument("retryRequest") ?: false + val retryRequest = call.argument("retryRequest") ?: false + permissionController.retryRequest = retryRequest - // + Log.i(TAG, "Method call: ${call.method}") when (call.method) { // Permissions - "permissionsStatus" -> result.success(onPermissionStatus()) - "permissionsRequest" -> onRequestPermission() + Method.PERMISSION_STATUS -> { + val hasPermission = permissionController.permissionStatus() + result.success(hasPermission) + } + Method.PERMISSION_REQUEST -> { + permissionController.requestPermission() + } // Device information - "queryDeviceInfo" -> queryDeviceInfo(result) + Method.QUERY_DEVICE_INFO -> { + result.success( + hashMapOf( + "device_model" to Build.MODEL, + "device_sys_version" to Build.VERSION.SDK_INT, + "device_sys_type" to "Android" + ) + ) + } // This method will scan the given path to update the 'state'. // When deleting a file using 'dart:io', call this method to update the file 'state'. - "scan" -> { + Method.SCAN -> { val sPath: String? = call.argument("path") + val context = PluginProvider.context() // Check if the given file is null or empty. - if (sPath == null || sPath.isEmpty()) result.success(false) + if (sPath == null || sPath.isEmpty()) { + Log.w(TAG, "Method 'scan' was called with null or empty 'path'") + result.success(false) + } // Scan and return - MediaScannerConnection.scanFile(pContext, arrayOf(sPath), null) { _, _ -> + MediaScannerConnection.scanFile(context, arrayOf(sPath), null) { _, _ -> + Log.d(TAG, "Scanned file: $sPath") result.success(true) } } + // Logging + Method.SET_LOG_CONFIG -> { + Log.setLogLevel(call.argument("level")!!) + result.success(true) + } + // All others methods - else -> onAudioController.onAudioController() + else -> { + Log.d(TAG, "Checking permissions...") + + val hasPermission = permissionController.permissionStatus() + Log.d(TAG, "Application has permissions: $hasPermission") + + if (!hasPermission) { + Log.w(TAG, "The application doesn't have access to the library") + result.error( + "MissingPermissions", + "Application doesn't have access to the library", + "Call the [permissionsRequest] method or install a external plugin to handle the app permission." + ) + } + + methodController.find() + } } + + Log.d(TAG, "Ended method call (${call.method})\n ") } - // This is only important for initialization - Start - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + Log.i(TAG, "Detached from engine") channel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { - this.pActivity = binding.activity - binding.addRequestPermissionsResultListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() {} - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} + Log.i(TAG, "Attached to activity") - override fun onDetachedFromActivity() {} - // End + // Init plugin provider with 'activity' and 'context'. + PluginProvider.set(binding.activity) - // OnPermissionController - // TODO Find another solution for Permission Request - - // - private val onRequestCode: Int = 88560 - - override fun onPermissionStatus(context: Context?): Boolean{ - // After "leaving" this class, context will be null so, we need this context argument to - // call the [checkSelfPermission]. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){ - return tiramisuPermission.all { - ContextCompat.checkSelfPermission( - context ?: pContext, - it - ) == PackageManager.PERMISSION_GRANTED - } - } - else { - return onPermission.all { - ContextCompat.checkSelfPermission( - context ?: pContext, - it - ) == PackageManager.PERMISSION_GRANTED - } - } + // Add to controller the permission to listen to the request result. + this.binding = binding + binding.addRequestPermissionsResultListener(permissionController) } - override fun onRequestPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){ - ActivityCompat.requestPermissions(pActivity, tiramisuPermission, onRequestCode) - } - else { - ActivityCompat.requestPermissions(pActivity, onPermission, onRequestCode) - } + override fun onDetachedFromActivityForConfigChanges() { + Log.i(TAG, "Detached from engine (config changes)") + onDetachedFromActivity() } - // Second requestPermission, this one with the option "Never Ask Again". - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun onRetryRequestPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){ - if (ActivityCompat.shouldShowRequestPermissionRationale(pActivity, onPermission[0]) - || ActivityCompat.shouldShowRequestPermissionRationale(pActivity, onPermission[1]) - ) { - retryRequest = false - onRequestPermission() - } - } - else { - if (ActivityCompat.shouldShowRequestPermissionRationale(pActivity, tiramisuPermission[0]) - || ActivityCompat.shouldShowRequestPermissionRationale(pActivity, tiramisuPermission[1]) - || ActivityCompat.shouldShowRequestPermissionRationale(pActivity,tiramisuPermission[2]) - ) { - retryRequest = false - onRequestPermission() - } - } + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + Log.i(TAG, "Reattached to activity (config changes)") + onAttachedToActivity(binding) } - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ): Boolean { - // When [pResult] is not initialized the permission request did not originate from the - // [on_audio_query] plugin, so return [false] to indicate the [on_audio_query] plugin is not - // handling the request result and Android should continue executing other registered handlers. - if (!this::pResult.isInitialized) return false - - // When the incoming request code doesn't match the request codes defined by the on_audio_query - // plugin return [false] to indicate the [on_audio_query] plugin is not handling the request - // result and Android should continue executing other registered handlers. - if (requestCode != onRequestCode) return false - - // Check permission - val isPermissionGranted = (grantResults.isNotEmpty() - && grantResults[0] == PackageManager.PERMISSION_GRANTED) - - // After all checks, we can handle the permission request. - when { - isPermissionGranted -> pResult.success(true) - retryRequest -> onRetryRequestPermission() - else -> pResult.success(false) + // Detach all parameters. + override fun onDetachedFromActivity() { + Log.i(TAG, "Detached from activity") + + // Remove the permission listener + if (binding != null) { + binding!!.removeRequestPermissionsResultListener(permissionController) } - // Return [true] here to indicate that the [on_audio_query] plugin handled the permission request - // result and Android should not continue executing other registered handlers. - return true + this.binding = null + Log.i(TAG, "Removed all declared methods") } } diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt new file mode 100644 index 00000000..f94a3049 --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt @@ -0,0 +1,90 @@ +package com.lucasjosino.on_audio_query + +import android.app.Activity +import android.content.Context +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.lang.ref.WeakReference + +/** + * A singleton used to define all variables/methods that will be used on all plugin. + * + * The singleton will provider the ability to 'request' required variables/methods on any moment. + * + * All variables/methods should be defined after plugin initialization (activity/context) and + * dart request (call/result). + */ +object PluginProvider { + private const val ERROR_MESSAGE = + "Tried to get one of the methods but the 'PluginProvider' has not initialized" + + private lateinit var context: WeakReference + + private lateinit var activity: WeakReference + + private lateinit var call: WeakReference + + private lateinit var result: WeakReference + + /** + * Used to define the current [Activity] and [Context]. + * + * Should be defined once. + */ + fun set(activity: Activity) { + this.context = WeakReference(activity.applicationContext) + this.activity = WeakReference(activity) + } + + /** + * Used to define the current dart request. + * + * Should be defined/redefined on every [MethodChannel.MethodCallHandler.onMethodCall] request. + */ + fun setCurrentMethod(call: MethodCall, result: MethodChannel.Result) { + this.call = WeakReference(call) + this.result = WeakReference(result) + } + + /** + * The current plugin 'context'. Defined once. + * + * @throws UninitializedPluginProviderException + * @return [Context] + */ + fun context(): Context { + return this.context.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'activity'. Defined once. + * + * @throws UninitializedPluginProviderException + * @return [Activity] + */ + fun activity(): Activity { + return this.activity.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'call'. Will be replace with newest dart request. + * + * @throws UninitializedPluginProviderException + * @return [MethodCall] + */ + fun call(): MethodCall { + return this.call.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'result'. Will be replace with newest dart request. + * + * @throws UninitializedPluginProviderException + * @return [MethodChannel.Result] + */ + fun result(): MethodChannel.Result { + return this.result.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + class UninitializedPluginProviderException(msg: String) : Exception(msg) +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt new file mode 100644 index 00000000..108cdb39 --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt @@ -0,0 +1,29 @@ +package com.lucasjosino.on_audio_query.consts + +object Method { + // General methods + const val PERMISSION_STATUS = "permissionsStatus" + const val PERMISSION_REQUEST = "permissionsRequest" + const val QUERY_DEVICE_INFO = "queryDeviceInfo" + const val SCAN = "scan" + const val SET_LOG_CONFIG = "setLogConfig" + + // Query methods + const val QUERY_AUDIOS = "querySongs" + const val QUERY_ALBUMS = "queryAlbums" + const val QUERY_ARTISTS = "queryArtists" + const val QUERY_GENRES = "queryGenres" + const val QUERY_PLAYLISTS = "queryPlaylists" + const val QUERY_ARTWORK = "queryArtwork" + const val QUERY_AUDIOS_FROM = "queryAudiosFrom" + const val QUERY_WITH_FILTERS = "queryWithFilters" + const val QUERY_ALL_PATHS = "queryAllPath" + + // Playlist methods + const val CREATE_PLAYLIST = "createPlaylist" + const val REMOVE_PLAYLIST = "removePlaylist" + const val ADD_TO_PLAYLIST = "addToPlaylist" + const val REMOVE_FROM_PLAYLIST = "removeFromPlaylist" + const val RENAME_PLAYLIST = "renamePlaylist" + const val MOVE_ITEM_TO = "moveItemTo" +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnAudioController.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnAudioController.kt deleted file mode 100644 index 1b3ba22b..00000000 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnAudioController.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.lucasjosino.on_audio_query.controller - -import android.content.Context -import android.util.Log -import com.lucasjosino.on_audio_query.query.* -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel - -class OnAudioController( - private val context: Context, - private val call: MethodCall, - private val result: MethodChannel.Result -) { - - // - fun onAudioController() { - when (call.method) { - //Query methods - "querySongs" -> OnAudiosQuery().querySongs(context, result, call) - "queryAlbums" -> OnAlbumsQuery().queryAlbums(context, result, call) - "queryArtists" -> OnArtistsQuery().queryArtists(context, result, call) - "queryPlaylists" -> OnPlaylistQuery().queryPlaylists(context, result, call) - "queryGenres" -> OnGenresQuery().queryGenres(context, result, call) - "queryArtwork" -> OnArtworksQuery().queryArtwork(context, result, call) - "queryAudiosFrom" -> OnAudiosFromQuery().querySongsFrom(context, result, call) - "queryWithFilters" -> OnWithFiltersQuery().queryWithFilters(context, result, call) - "queryAllPath" -> OnAllPathQuery().queryAllPath(context, result) - //Playlists methods - "createPlaylist" -> OnPlaylistsController().createPlaylist(context, result, call) - "removePlaylist" -> OnPlaylistsController().removePlaylist(context, result, call) - "addToPlaylist" -> OnPlaylistsController().addToPlaylist(context, result, call) - "removeFromPlaylist" -> OnPlaylistsController().removeFromPlaylist( - context, - result, - call - ) - "renamePlaylist" -> OnPlaylistsController().renamePlaylist(context, result, call) - "moveItemTo" -> OnPlaylistsController().moveItemTo(context, result, call) - } - } -} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt new file mode 100644 index 00000000..608d8629 --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt @@ -0,0 +1,32 @@ +package com.lucasjosino.on_audio_query.controllers + +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.consts.Method +import com.lucasjosino.on_audio_query.queries.* + +class MethodController() { + + // + fun find() { + when (PluginProvider.call().method) { + //Query methods + Method.QUERY_AUDIOS -> AudioQuery().querySongs() + Method.QUERY_ALBUMS -> AlbumQuery().queryAlbums() + Method.QUERY_ARTISTS -> ArtistQuery().queryArtists() + Method.QUERY_PLAYLISTS -> PlaylistQuery().queryPlaylists() + Method.QUERY_GENRES -> GenreQuery().queryGenres() + Method.QUERY_ARTWORK -> ArtworkQuery().queryArtwork() + Method.QUERY_AUDIOS_FROM -> AudioFromQuery().querySongsFrom() + Method.QUERY_WITH_FILTERS -> WithFiltersQuery().queryWithFilters() + Method.QUERY_ALL_PATHS -> AllPathQuery().queryAllPath() + //Playlists methods + Method.CREATE_PLAYLIST -> PlaylistController().createPlaylist() + Method.REMOVE_PLAYLIST -> PlaylistController().removePlaylist() + Method.ADD_TO_PLAYLIST -> PlaylistController().addToPlaylist() + Method.REMOVE_FROM_PLAYLIST -> PlaylistController().removeFromPlaylist() + Method.RENAME_PLAYLIST -> PlaylistController().renamePlaylist() + Method.MOVE_ITEM_TO -> PlaylistController().moveItemTo() + else -> PluginProvider.result().notImplemented() + } + } +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt new file mode 100644 index 00000000..5080b474 --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt @@ -0,0 +1,93 @@ +package com.lucasjosino.on_audio_query.controllers + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.interfaces.PermissionManagerInterface +import io.flutter.Log +import io.flutter.plugin.common.PluginRegistry + +class PermissionController : PermissionManagerInterface, + PluginRegistry.RequestPermissionsResultListener { + + companion object { + private const val TAG: String = "PermissionController" + + private const val REQUEST_CODE: Int = 88560 + } + + var retryRequest: Boolean = false + + private var permissions: Array = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.READ_MEDIA_AUDIO, + Manifest.permission.READ_MEDIA_IMAGES + ) + } else { + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + + override fun permissionStatus(): Boolean = permissions.all { + // After "leaving" this class, context will be null so, we need this context argument to + // call the [checkSelfPermission]. + return ContextCompat.checkSelfPermission( + PluginProvider.context(), + it + ) == PackageManager.PERMISSION_GRANTED + } + + override fun requestPermission() { + Log.d(TAG, "Requesting permissions.") + Log.d(TAG, "SDK: ${Build.VERSION.SDK_INT}, Should retry request: $retryRequest") + val activity = PluginProvider.activity() + ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) + } + + // Second requestPermission, this one with the option "Never Ask Again". + override fun retryRequestPermission() { + val activity = PluginProvider.activity() + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permissions[0]) + || ActivityCompat.shouldShowRequestPermissionRationale(activity, permissions[1]) + ) { + Log.d(TAG, "Retrying permission request") + retryRequest = false + requestPermission() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + // When the incoming request code doesn't match the request codes defined by the on_audio_query + // plugin return [false] to indicate the [on_audio_query] plugin is not handling the request + // result and Android should continue executing other registered handlers. + if (REQUEST_CODE != requestCode) return false + + // Check permission + val isPermissionGranted = (grantResults.isNotEmpty() + && grantResults[0] == PackageManager.PERMISSION_GRANTED) + + Log.d(TAG, "Permission accepted: $isPermissionGranted") + + // After all checks, we can handle the permission request. + val result = PluginProvider.result() + when { + isPermissionGranted -> result.success(true) + retryRequest -> retryRequestPermission() + else -> result.success(false) + } + + // Return [true] here to indicate that the [on_audio_query] plugin handled the permission request + // result and Android should not continue executing other registered handlers. + return true + } +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnPlaylistsController.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt similarity index 88% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnPlaylistsController.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt index ba105892..1bf99581 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controller/OnPlaylistsController.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt @@ -1,17 +1,15 @@ -package com.lucasjosino.on_audio_query.controller +package com.lucasjosino.on_audio_query.controllers import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues -import android.content.Context import android.os.Build import android.provider.MediaStore import android.util.Log -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import com.lucasjosino.on_audio_query.PluginProvider /** OnPlaylistsController */ -class OnPlaylistsController { +class PlaylistController { //Main parameters private val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI @@ -24,8 +22,12 @@ class OnPlaylistsController { "count(*)" ) + private val context = PluginProvider.context() + private val result = PluginProvider.result() + private val call = PluginProvider.call() + // - fun createPlaylist(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun createPlaylist() { this.resolver = context.contentResolver val playlistName = call.argument("playlistName")!! @@ -37,7 +39,7 @@ class OnPlaylistsController { } // - fun removePlaylist(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun removePlaylist() { this.resolver = context.contentResolver val playlistId = call.argument("playlistId")!! @@ -52,7 +54,7 @@ class OnPlaylistsController { //TODO Add option to use a list //TODO Fix error on Android 10 - fun addToPlaylist(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun addToPlaylist() { this.resolver = context.contentResolver val playlistId = call.argument("playlistId")!! val audioId = call.argument("audioId")!! @@ -84,7 +86,7 @@ class OnPlaylistsController { } //TODO Add option to use a list - fun removeFromPlaylist(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun removeFromPlaylist() { this.resolver = context.contentResolver val playlistId = call.argument("playlistId")!! val audioId = call.argument("audioId")!! @@ -108,7 +110,7 @@ class OnPlaylistsController { } //TODO("Need tests") - fun moveItemTo(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun moveItemTo() { this.resolver = context.contentResolver val playlistId = call.argument("playlistId")!! val from = call.argument("from")!! @@ -123,7 +125,7 @@ class OnPlaylistsController { } // - fun renamePlaylist(context: Context, result: MethodChannel.Result, call: MethodCall) { + fun renamePlaylist() { this.resolver = context.contentResolver val playlistId = call.argument("playlistId")!! val newPlaylistName = call.argument("newPlName")!! diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/OnPermissionManagerInterface.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/OnPermissionManagerInterface.kt deleted file mode 100644 index e581f7d0..00000000 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/OnPermissionManagerInterface.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.lucasjosino.on_audio_query.interfaces - -import android.content.Context - -/** OnPermissionManagerInterface */ -interface OnPermissionManagerInterface { - fun onPermissionStatus(context: Context? = null) : Boolean - fun onRequestPermission() - fun onRetryRequestPermission() -} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt new file mode 100644 index 00000000..b0bdab9a --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt @@ -0,0 +1,7 @@ +package com.lucasjosino.on_audio_query.interfaces + +interface PermissionManagerInterface { + fun permissionStatus() : Boolean + fun requestPermission() + fun retryRequestPermission() +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAlbumsQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt similarity index 52% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAlbumsQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt index 689ead37..dea02450 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAlbumsQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt @@ -1,44 +1,39 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries import android.content.ContentResolver -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkAlbumsUriType import com.lucasjosino.on_audio_query.types.sorttypes.checkAlbumSortType -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** OnAlbumsQuery */ -class OnAlbumsQuery : ViewModel() { +class AlbumQuery : ViewModel() { + + companion object { + private const val TAG = "OnAlbumsQuery" + } // Main parameters. - private val helper = OnAudioHelper() + private val helper = QueryHelper() - // None of this methods can be null. private lateinit var uri: Uri private lateinit var sortType: String private lateinit var resolver: ContentResolver /** * Method to "query" all albums. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun queryAlbums( - context: Context, - result: MethodChannel.Result, - call: MethodCall - ) { + fun queryAlbums() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() this.resolver = context.contentResolver // Sort: Type and Order. @@ -47,69 +42,51 @@ class OnAlbumsQuery : ViewModel() { call.argument("orderType")!!, call.argument("ignoreCase")!! ) + // Check uri: - // * [0]: External. - // * [1]: Internal. + // * 0 -> External + // * 1 -> Internal uri = checkAlbumsUriType(call.argument("uri")!!) + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultAlbumList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultAlbumList = loadAlbums() - } - - // Flutter UI will start, but, information still loading. - result.success(resultAlbumList) + val queryResult = loadAlbums() + result.success(queryResult) } } // Loading in Background private suspend fun loadAlbums(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri], [projection](null == all items) and [sortType]. + // Setup the cursor with 'uri', 'projection'(null == all items) and 'sortType'. val cursor = resolver.query(uri, null, null, null, sortType) - // Empty list. + val albumList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item(album) inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val tempData: MutableMap = HashMap() + for (albumMedia in cursor.columnNames) { tempData[albumMedia] = helper.loadAlbumItem(albumMedia, cursor) } - // In Android 10 and above [album_art] will return null, to avoid problem, - // we remove it. Use [queryArtwork] instead. + + // Android 10 and above 'album_art' will return null. Use 'queryArtwork' instead. val art = tempData["album_art"].toString() if (art.isEmpty()) tempData.remove("album_art") + albumList.add(tempData) } // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext albumList } } - -//I/AlbumCursor: [ -// numsongs, -// artist, -// numsongs_by_artist, -// _id, -// album, -// album_art, -// album_key, -// artist_id, -// maxyear, -// minyear, -// album_id, -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt new file mode 100644 index 00000000..aa8c8e0f --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt @@ -0,0 +1,60 @@ +package com.lucasjosino.on_audio_query.queries + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import com.lucasjosino.on_audio_query.PluginProvider +import io.flutter.Log +import java.io.File + +/** OnAllPathQuery */ +class AllPathQuery { + + companion object { + private const val TAG = "OnAllPathQuery" + + private val URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all paths. + */ + fun queryAllPath() { + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + val resultAllPath = loadAllPath() + result.success(resultAllPath) + } + + // Ignore the '_data' deprecation because this plugin support older versions. + @SuppressLint("Range") + @Suppress("DEPRECATION") + private fun loadAllPath(): ArrayList { + val cursor = resolver.query(URI, null, null, null, null) + + val songPathList: ArrayList = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(path) inside this "cursor", take one and add to the list. + while (cursor != null && cursor.moveToNext()) { + val content = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)) + + val path = File(content).parent + + // Check if path is null or if already exist inside list. + if (path != null && !songPathList.contains(path)) { + songPathList.add(path) + } + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return songPathList + } +} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtistsQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt similarity index 55% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtistsQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt index 91ded344..d4e0abbd 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtistsQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt @@ -1,26 +1,29 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries import android.content.ContentResolver -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkArtistsUriType import com.lucasjosino.on_audio_query.types.sorttypes.checkArtistSortType import com.lucasjosino.on_audio_query.utils.artistProjection -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** OnArtistsQuery */ -class OnArtistsQuery : ViewModel() { +class ArtistQuery : ViewModel() { + + companion object { + private const val TAG = "OnArtistsQuery" + } //Main parameters - private val helper = OnAudioHelper() + private val helper = QueryHelper() // None of this methods can be null. private lateinit var uri: Uri @@ -29,14 +32,12 @@ class OnArtistsQuery : ViewModel() { /** * Method to "query" all artists. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun queryArtists(context: Context, result: MethodChannel.Result, call: MethodCall) { - resolver = context.contentResolver + fun queryArtists() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver // Sort: Type and Order sortType = checkArtistSortType( @@ -44,41 +45,38 @@ class OnArtistsQuery : ViewModel() { call.argument("orderType")!!, call.argument("ignoreCase")!! ) + // Check uri: - // * [0]: External. - // * [1]: Internal. + // * 0 -> External. + // * 1 -> Internal. uri = checkArtistsUriType(call.argument("uri")!!) + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultArtistList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultArtistList = loadArtists() - } - - //Flutter UI will start, but, information still loading - result.success(resultArtistList) + val queryResult = loadArtists() + result.success(queryResult) } } // Loading in Background private suspend fun loadArtists(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri], [projection] and [sortType]. + // Setup the cursor with 'uri', 'projection' and 'sortType'. val cursor = resolver.query(uri, artistProjection, null, null, sortType) - // Empty list. + val artistList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item(artist) inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val tempData: MutableMap = HashMap() + for (artistMedia in cursor.columnNames) { tempData[artistMedia] = helper.loadArtistItem(artistMedia, cursor) } @@ -88,18 +86,6 @@ class OnArtistsQuery : ViewModel() { // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext artistList } } - -//Extras: - -//I/OnArtistCursor[All/Audio]: [ -// _id -// artist -// artist_key -// number_of_albums -// number_of_tracks -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtworksQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt similarity index 52% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtworksQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt index 3788c7d6..dc157f16 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnArtworksQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt @@ -1,8 +1,7 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries import android.content.ContentResolver import android.content.ContentUris -import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever @@ -11,12 +10,11 @@ import android.os.Build import android.util.Size import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkArtworkFormat import com.lucasjosino.on_audio_query.types.checkArtworkType -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -24,70 +22,73 @@ import java.io.ByteArrayOutputStream import java.io.FileInputStream /** OnArtworksQuery */ -class OnArtworksQuery : ViewModel() { +class ArtworkQuery : ViewModel() { + + companion object { + private const val TAG = "OnArtworksQuery" + } //Main parameters - private val helper = OnAudioHelper() + private val helper = QueryHelper() private var type: Int = -1 private var id: Number = 0 private var quality: Int = 100 private var size: Int = 200 - // None of this methods can be null. private lateinit var uri: Uri private lateinit var resolver: ContentResolver private lateinit var format: Bitmap.CompressFormat /** * Method to "query" all albums. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun queryArtwork(context: Context, result: MethodChannel.Result, call: MethodCall) { - resolver = context.contentResolver + fun queryArtwork() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + id = call.argument("id")!! + + // If the 'size' is null, will be '200'. + size = call.argument("size")!! - // The [id] of the song/album. If the [size] is null, will be [200]. - id = call.argument("id")!!; size = call.argument("size")!! - // Define the quality of image. - // The [quality] value cannot be greater than 100 so, we check and if is, set to [100]. + // The 'quality' value cannot be greater than 100 so, we check and if is, set to '50'. quality = call.argument("quality")!! - if (quality > 100) quality = 100 + if (quality > 100) quality = 50 + // Check format: - // * [0]: JPEG - // * [1]: PNG + // * 0 -> JPEG + // * 1 -> PNG format = checkArtworkFormat(call.argument("format")!!) + // Check uri: - // * [0]: Song. - // * [1]: Album. - // * [2]: Playlist. - // * [3]: Artist. - // * [4]: Genre. + // * 0 -> Song. + // * 1 -> Album. + // * 2 -> Playlist. + // * 3 -> Artist. + // * 4 -> Genre. uri = checkArtworkType(call.argument("type")!!) - // Define the [type]: + type = call.argument("type")!! + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tid: $id") + Log.d(TAG, "\tquality: $quality") + Log.d(TAG, "\tformat: $format") + Log.d(TAG, "\turi: $uri") + Log.d(TAG, "\ttype: $type") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty array. - var resultArtList: ByteArray? = null - - // We cannot "query" without permission so, just return null. - if (hasPermission) { - // Start querying - resultArtList = loadArt() - } + var resultArtList = loadArt() // Sometimes android will extract a 'wrong' or 'empty' artwork. Just set as null. if (resultArtList != null && resultArtList.isEmpty()) { + Log.i(TAG, "Artwork for '$id' is empty. Returning null") resultArtList = null } - // Flutter UI will start, but, information still loading result.success(resultArtList) } } @@ -95,28 +96,23 @@ class OnArtworksQuery : ViewModel() { //Loading in Background @Suppress("BlockingMethodInNonBlockingContext") private suspend fun loadArt(): ByteArray? = withContext(Dispatchers.IO) { - // Empty array. var artData: ByteArray? = null - // In this case we need check the [Android] version and query [type]. - // - // If [Android] >= 29/Q: - // * We have a limited access to files/folders and we use [loadThumbnail]. - // If [Android] < 29/Q: - // * We use the [embeddedPicture] from [MediaMetadataRetriever] to get the image. + // If 'Android' >= 29/Q: + // * Limited access to files/folders. Use 'loadThumbnail'. + // If 'Android' < 29/Q: + // * Use the 'embeddedPicture' from 'MediaMetadataRetriever' to get the image. if (Build.VERSION.SDK_INT >= 29) { - // Try / Catch to avoid problems. try { - // If [type] is 2, 3 or 4, we need to 'get' the first item from playlist or artist. - // We'll use the first artist song to 'simulate' the artwork. + // If 'type' is 2, 3 or 4, Get the first item from playlist or artist. + // Use the first artist song to 'simulate' the artwork. // // Type: - // * [2]: Playlist. - // * [3]: Artist. - // * [4]: Genre. + // * 2 -> Playlist. + // * 3 -> Artist. + // * 4 -> Genre. // - // Due old problems with [MethodChannel] the [id] is defined as [Number]. - // Here we convert to [Long] + // OBS: The 'id' is defined as 'Number'. Convert to 'Long' val query = if (type == 2 || type == 3 || type == 4) { val item = helper.loadFirstItem(type, id, resolver) ?: return@withContext null ContentUris.withAppendedId(uri, item.toLong()) @@ -127,40 +123,34 @@ class OnArtworksQuery : ViewModel() { val bitmap = resolver.loadThumbnail(query, Size(size, size), null) artData = convertOrResize(bitmap = bitmap)!! } catch (e: Exception) { - // Some problem can occur, we hide to not "flood" the terminal. -// Log.i("on_audio_error: ", e.toString()) + Log.w(TAG, "($id) Message: $e") } } else { - // If [uri == Audio]: - // * Load the first [item] from cursor using the [id] as filter. + // If 'uri == Audio': + // * Load the first 'item' from cursor using the 'id' as filter. // else: - // * Load the first [item] from [album] using the [id] as filter. + // * Load the first 'item' from 'album' using the 'id' as filter. // - // If [item] return null, no song/album has found, just return null. + // If 'item' return null, no song/album has found, return null. val item = helper.loadFirstItem(type, id, resolver) ?: return@withContext null + try { - // I tried both [_data] and [_uri], none of them work. - // So we use the [_data] inside the [FileInputStream] and take the - // [fd(FileDescriptor)]. val file = FileInputStream(item) val metadata = MediaMetadataRetriever() - // Most of the cases the error occurred here. metadata.setDataSource(file.fd) val image = metadata.embeddedPicture - // Check if [image] null. + // Convert image. If null, return artData = convertOrResize(byteArray = image) ?: return@withContext null - // [close] can only be called using [Android] >= 29/Q. + // 'close' can only be called using 'Android' >= 29/Q. if (Build.VERSION.SDK_INT >= 29) metadata.close() } catch (e: Exception) { - // Some problem can occur, we hide to not "flood" the terminal. -// Log.i("on_audio_error: ", e.toString()) + Log.w(TAG, "($id) Message: $e") } } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). + return@withContext artData } @@ -168,8 +158,9 @@ class OnArtworksQuery : ViewModel() { private fun convertOrResize(bitmap: Bitmap? = null, byteArray: ByteArray? = null): ByteArray? { val convertedBytes: ByteArray? val byteArrayBase = ByteArrayOutputStream() + try { - // If [bitmap] isn't null: + // If 'bitmap' isn't null: // * The image(bitmap) is from first method. (Android >= 29/Q). // else: // * The image(bytearray) is from second method. (Android < 29/Q). @@ -180,8 +171,9 @@ class OnArtworksQuery : ViewModel() { convertedBitmap.compress(format, quality, byteArrayBase) } } catch (e: Exception) { - //Log.i("Error", e.toString()) + Log.w(TAG, "($id) Message: $e") } + convertedBytes = byteArrayBase.toByteArray() byteArrayBase.close() return convertedBytes diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosFromQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt similarity index 59% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosFromQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt index 672fb123..00c0933c 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosFromQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt @@ -1,18 +1,18 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries -import android.annotation.SuppressLint import android.content.ContentResolver -import android.content.Context import android.net.Uri import android.os.Build import android.provider.MediaStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkAudiosFromType import com.lucasjosino.on_audio_query.types.sorttypes.checkSongSortType import com.lucasjosino.on_audio_query.utils.songProjection +import io.flutter.Log import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.Dispatchers @@ -20,17 +20,20 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** OnAudiosFromQuery */ -class OnAudiosFromQuery : ViewModel() { +class AudioFromQuery : ViewModel() { + + companion object { + private const val TAG = "OnAudiosFromQuery" + + private val URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } //Main parameters - private val helper = OnAudioHelper() - private val uri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + private val helper = QueryHelper() private var pId = 0 private var pUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI // None of this methods can be null. - @SuppressLint("StaticFieldLeak") - private lateinit var context: Context private lateinit var where: String private lateinit var whereVal: String private lateinit var sortType: String @@ -38,67 +41,59 @@ class OnAudiosFromQuery : ViewModel() { /** * Method to "query" all songs from a specific item. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun querySongsFrom(context: Context, result: MethodChannel.Result, call: MethodCall) { - this.context = context; resolver = context.contentResolver - - // The type of [item]: - // * [0]: Album - // * [1]: Album Id - // * [2]: Artist - // * [3]: Artist Id - // * [4]: Genre - // * [5]: Genre Id - // * [6]: Playlist + fun querySongsFrom() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // The type of 'item': + // * 0 -> Album + // * 1 -> Album Id + // * 2 -> Artist + // * 3 -> Artist Id + // * 4 -> Genre + // * 5 -> Genre Id + // * 6 -> Playlist val type = call.argument("type")!! + // Sort: Type and Order. - // TODO sortType = checkSongSortType( call.argument("sortType"), call.argument("orderType")!!, call.argument("ignoreCase")!! ) + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\ttype: $type") + Log.d(TAG, "\turi: $URI") + // TODO: Add a better way to handle this query // This will fix (for now) the problem between Android < 30 && Android > 30 // The method used to query genres on Android >= 30 don't work properly on Android < 30 so, // we need separate. // - // If helper == 6 (Playlist) send to [querySongsFromPlaylistOrGenre] in any version. + // If helper == 6 (Playlist) send to 'querySongsFromPlaylistOrGenre' in any version. // If helper == 4 (Genre) || helper == 5 (GenreId) and Android < 30 send to - // [querySongsFromPlaylistOrGenre] else, follow the rest of the "normal" code. + // 'querySongsFromPlaylistOrGenre' else, follow the rest of the "normal" code. // - // Why? Android 10 and below don't has "genre" category and we need use a "workaround". - // [MediaStore](https://developer.android.com/reference/android/provider/MediaStore.Audio.AudioColumns#GENRE) + // Why? Android 10 and below doesn't have "genre" category and we need use a "workaround". + // 'MediaStore'(https://developer.android.com/reference/android/provider/MediaStore.Audio.AudioColumns#GENRE) if (type == 6 || ((type == 4 || type == 5) && Build.VERSION.SDK_INT < 30)) { - // Works on [Android] 10. + // Works on Android 10. querySongsFromPlaylistOrGenre(result, call, type) } else { - // Works on [Android] 11. - // [whereVal] -> Album/Artist/Genre(Sometimes) - // [where] -> uri + // Works on Android 11. + // 'whereVal' -> Album/Artist/Genre + // 'where' -> uri whereVal = call.argument("where")!!.toString() where = checkAudiosFromType(type) // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultSongList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultSongList = loadSongsFrom() - } - - //Flutter UI will start, but, information still loading + val resultSongList = loadSongsFrom() result.success(resultSongList) } } @@ -107,13 +102,15 @@ class OnAudiosFromQuery : ViewModel() { //Loading in Background private suspend fun loadSongsFrom(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri], [projection], [selection](where) and [values](whereVal). - val cursor = resolver.query(uri, songProjection(), where, arrayOf(whereVal), sortType) - // Empty list. + // Setup the cursor with 'uri', 'projection', 'selection'(where) and 'values'(whereVal). + val cursor = resolver.query(URI, songProjection(), where, arrayOf(whereVal), sortType) + val songsFromList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item(song) inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val tempData: MutableMap = HashMap() for (audioMedia in cursor.columnNames) { @@ -121,17 +118,14 @@ class OnAudiosFromQuery : ViewModel() { } //Get a extra information from audio, e.g: extension, uri, etc.. - val tempExtraData = helper.loadSongExtraInfo(uri, tempData) + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) tempData.putAll(tempExtraData) - // songsFromList.add(tempData) } // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext songsFromList } @@ -143,63 +137,59 @@ class OnAudiosFromQuery : ViewModel() { ) { val info = call.argument("where")!! - //Check if Playlist exists based in Id + // Check if playlist exists using the id. val checkedName = if (type == 4 || type == 5) { checkName(genreName = info.toString()) - } else checkName(plName = info.toString()) + } else { + checkName(plName = info.toString()) + } if (!checkedName) pId = info.toString().toInt() - // pUri = if (type == 4 || type == 5) { MediaStore.Audio.Genres.Members.getContentUri("external", pId.toLong()) - } else MediaStore.Audio.Playlists.Members.getContentUri("external", pId.toLong()) + } else { + MediaStore.Audio.Playlists.Members.getContentUri("external", pId.toLong()) + } // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultSongsFrom = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultSongsFrom = loadSongsFromPlaylistOrGenre() - } - - //Flutter UI will start, but, information still loading + val resultSongsFrom = loadSongsFromPlaylistOrGenre() result.success(resultSongsFrom) } } private suspend fun loadSongsFromPlaylistOrGenre(): ArrayList> = withContext(Dispatchers.IO) { - val songsFrom: ArrayList> = ArrayList() + val cursor = resolver.query(pUri, songProjection(), null, null, sortType) + + Log.d(TAG, "Cursor count: ${cursor?.count}") + while (cursor != null && cursor.moveToNext()) { val tempData: MutableMap = HashMap() + for (media in cursor.columnNames) { tempData[media] = helper.loadSongItem(media, cursor) } //Get a extra information from audio, e.g: extension, uri, etc.. - val tempExtraData = helper.loadSongExtraInfo(uri, tempData) + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) tempData.putAll(tempExtraData) songsFrom.add(tempData) } + cursor?.close() return@withContext songsFrom } - //Return true if playlist or genre exists, false, if don't. + // Return true if playlist or genre exists and false, if don't. private fun checkName(plName: String? = null, genreName: String? = null): Boolean { val uri: Uri val projection: Array - // if (plName != null) { uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI projection = arrayOf(MediaStore.Audio.Playlists.NAME, MediaStore.Audio.Playlists._ID) @@ -208,46 +198,17 @@ class OnAudiosFromQuery : ViewModel() { projection = arrayOf(MediaStore.Audio.Genres.NAME, MediaStore.Audio.Genres._ID) } - // val cursor = resolver.query(uri, projection, null, null, null) while (cursor != null && cursor.moveToNext()) { - val name = cursor.getString(0) //Name + val name = cursor.getString(0) if (name != null && name == plName || name == genreName) { pId = cursor.getInt(1) return true } } + cursor?.close() return false } } - -//Extras: - -// * All projection used for query audio in this Plugin -//I/OnAudioCursor[Audio]: [ -// _data, -// _display_name, -// _id, -// _size, -// album, -// album_artist, -// album_id -// album_key, -// artist, -// artist_id, -// artist_key, -// bookmark, -// composer, -// date_added, -// duration, -// title, -// track, -// year, -// is_alarm -// is_music, -// is_notification, -// is_podcast, -// is_ringtone -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt new file mode 100644 index 00000000..ef218947 --- /dev/null +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt @@ -0,0 +1,102 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkAudiosUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkSongSortType +import com.lucasjosino.on_audio_query.utils.songProjection +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnAudiosQuery */ +class AudioQuery : ViewModel() { + + companion object { + private const val TAG = "OnAudiosQuery" + } + + // Main parameters + private val helper = QueryHelper() + private var selection: String? = null + + private lateinit var uri: Uri + private lateinit var sortType: String + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all songs. + */ + @Suppress("DEPRECATION") + fun querySongs() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order. + sortType = checkSongSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + // Check uri: + // * 0 -> External. + // * 1 -> Internal. + uri = checkAudiosUriType(call.argument("uri")!!) + + // Here we provide a custom 'path'. + if (call.argument("path") != null) { + val projection = songProjection() + selection = projection[0] + " like " + "'%" + call.argument("path") + "/%'" + } + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\tselection: $selection") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadSongs() + result.success(queryResult) + } + } + + //Loading in Background + private suspend fun loadSongs(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection' and 'sortType'. + val cursor = resolver.query(uri, songProjection(), selection, null, sortType) + + val songList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(song) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (audioMedia in cursor.columnNames) { + tempData[audioMedia] = helper.loadSongItem(audioMedia, cursor) + } + + //Get a extra information from audio, e.g: extension, uri, etc.. + val tempExtraData = helper.loadSongExtraInfo(uri, tempData) + tempData.putAll(tempExtraData) + + songList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext songList + } +} diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnGenresQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt similarity index 58% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnGenresQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt index dabab8c0..536e991f 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnGenresQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt @@ -1,42 +1,41 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries import android.content.ContentResolver -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkGenresUriType import com.lucasjosino.on_audio_query.types.sorttypes.checkGenreSortType import com.lucasjosino.on_audio_query.utils.genreProjection -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** OnGenresQuery */ -class OnGenresQuery : ViewModel() { +class GenreQuery : ViewModel() { + + companion object { + private const val TAG = "OnGenresQuery" + } // Main parameters. - private val helper = OnAudioHelper() + private val helper = QueryHelper() - // None of this methods can be null. private lateinit var uri: Uri private lateinit var sortType: String private lateinit var resolver: ContentResolver /** * Method to "query" all genres. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun queryGenres(context: Context, result: MethodChannel.Result, call: MethodCall) { - resolver = context.contentResolver + fun queryGenres() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver // Sort: Type and Order. sortType = checkGenreSortType( @@ -44,41 +43,38 @@ class OnGenresQuery : ViewModel() { call.argument("orderType")!!, call.argument("ignoreCase")!! ) + // Check uri: - // * [0]: External. - // * [1]: Internal. + // * 0 -> External + // * 1 -> Internal uri = checkGenresUriType(call.argument("uri")!!) + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultGenreList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultGenreList = loadGenres() - } - - //Flutter UI will start, but, information still loading - result.success(resultGenreList) + val queryResult = loadGenres() + result.success(queryResult) } } - //Loading in Background + // Loading in Background private suspend fun loadGenres(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri], [projection] and [sortType]. + // Setup the cursor with 'uri', 'projection' and 'sortType'. val cursor = resolver.query(uri, genreProjection, null, null, sortType) - // Empty list. + val genreList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item(genre) inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val genreData: MutableMap = HashMap() + for (genreMedia in cursor.columnNames) { genreData[genreMedia] = helper.loadGenreItem(genreMedia, cursor) } @@ -94,15 +90,6 @@ class OnGenresQuery : ViewModel() { // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext genreList } } - -//Extras: - -//I/OnGenreCursor[All/Audio]: [ -// _id -// name -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnPlaylistQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt similarity index 57% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnPlaylistQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt index 86bc9f87..e7501f67 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnPlaylistQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt @@ -1,42 +1,42 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries import android.content.ContentResolver -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.checkPlaylistsUriType import com.lucasjosino.on_audio_query.types.sorttypes.checkGenreSortType import com.lucasjosino.on_audio_query.utils.playlistProjection -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** OnPlaylistQuery */ -class OnPlaylistQuery : ViewModel() { +class PlaylistQuery : ViewModel() { + + companion object { + private const val TAG = "OnPlaylistQuery" + } //Main parameters - private val helper = OnAudioHelper() + private val helper = QueryHelper() - // None of this methods can be null. private lateinit var uri: Uri private lateinit var resolver: ContentResolver private lateinit var sortType: String /** * Method to "query" all playlists. - * - * Parameters: - * * [context] - * * [result] - * * [call] */ - fun queryPlaylists(context: Context, result: MethodChannel.Result, call: MethodCall) { - resolver = context.contentResolver + fun queryPlaylists() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver // Sort: Type and Order. sortType = checkGenreSortType( @@ -45,40 +45,36 @@ class OnPlaylistQuery : ViewModel() { call.argument("ignoreCase")!! ) // Check uri: - // * [0]: External. - // * [1]: Internal. + // * 0 -> External. + // * 1 -> Internal. uri = checkPlaylistsUriType(call.argument("uri")!!) + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultPlaylistList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultPlaylistList = loadPlaylists() - } - - //Flutter UI will start, but, information still loading - result.success(resultPlaylistList) + val queryResult = loadPlaylists() + result.success(queryResult) } } //Loading in Background private suspend fun loadPlaylists(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri] and [projection]. + // Setup the cursor with 'uri' and 'projection'. val cursor = resolver.query(uri, playlistProjection, null, null, null) - // Empty list. + val playlistList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item(playlist) inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val playlistData: MutableMap = HashMap() + for (playlistMedia in cursor.columnNames) { playlistData[playlistMedia] = helper.loadPlaylistItem(playlistMedia, cursor) } @@ -92,18 +88,6 @@ class OnPlaylistQuery : ViewModel() { // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext playlistList } } - -//Extras: - -//I/OnPlaylistCursor[All/Audio]: [ -// _data -// _id -// date_added -// date_modified -// name -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnWithFiltersQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt similarity index 60% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnWithFiltersQuery.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt index a67cca42..ed80ce35 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnWithFiltersQuery.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt @@ -1,55 +1,57 @@ -package com.lucasjosino.on_audio_query.query +package com.lucasjosino.on_audio_query.queries -import android.annotation.SuppressLint import android.content.ContentResolver -import android.content.Context import android.net.Uri import android.provider.MediaStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper import com.lucasjosino.on_audio_query.types.* -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel +import io.flutter.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class OnWithFiltersQuery : ViewModel() { +class WithFiltersQuery : ViewModel() { + + companion object { + private const val TAG = "OnWithFiltersQuery" + + private val URI = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } //Main parameters - private val helper = OnAudioHelper() - private val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + private val helper = QueryHelper() private var projection: Array? = arrayOf() - @SuppressLint("StaticFieldLeak") - // None of this methods can be null. - private lateinit var context: Context private lateinit var resolver: ContentResolver private lateinit var withType: Uri private lateinit var argsVal: String private lateinit var argsKey: String // - fun queryWithFilters(context: Context, result: MethodChannel.Result, call: MethodCall) { - this.context = context; resolver = context.contentResolver + fun queryWithFilters() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver // Choose the type. - // * [0]: Audios - // * [1]: Albums - // * [2]: Playlists - // * [3]: Artists - // * [4]: Genres + // * 0 -> Audios + // * 1 -> Albums + // * 2 -> Playlists + // * 3 -> Artists + // * 4 -> Genres withType = checkWithFiltersType(call.argument("withType")!!) - // The [args] are converted to [String] before send to [MethodChannel]. + // The 'args' are converted to 'String' before send to 'MethodChannel'. argsVal = "%" + call.argument("argsVal")!! + "%" - // A dynamic [projection] to every type of "query". + // A dynamic 'projection' to every type of "query". projection = checkProjection(withType) - // Choose the [arg]. + // Choose the 'arg'. argsKey = when (withType) { MediaStore.Audio.Media.EXTERNAL_CONTENT_URI -> checkSongsArgs(call.argument("args")!!) MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI -> checkAlbumsArgs(call.argument("args")!!) @@ -63,36 +65,33 @@ class OnWithFiltersQuery : ViewModel() { else -> throw Exception("[argsKey] returned null. Report this issue on [on_audio_query] GitHub.") } + Log.d(TAG, "Query config: ") + Log.d(TAG, "\twithType: $withType") + Log.d(TAG, "\targsVal: $argsVal") + Log.d(TAG, "\targsKey: $argsKey") + // Query everything in background for a better performance. viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultWithFilter = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultWithFilter = loadWithFilters() - } - - //Flutter UI will start, but, information still loading - result.success(resultWithFilter) + val queryResult = loadWithFilters() + result.success(queryResult) } } //Loading in Background private suspend fun loadWithFilters(): ArrayList> = withContext(Dispatchers.IO) { - // Setup the cursor with [uri], [projection], [argsKey] and [argsVal]. + // Setup the cursor with 'uri', 'projection', 'argsKey' and 'argsVal'. val cursor = resolver.query(withType, projection, argsKey, arrayOf(argsVal), null) - // Empty list. + val withFiltersList: ArrayList> = ArrayList() + Log.d(TAG, "Cursor count: ${cursor?.count}") + // For each item inside this "cursor", take one and "format" - // into a [Map]. + // into a 'Map'. while (cursor != null && cursor.moveToNext()) { val tempData: MutableMap = HashMap() + for (media in cursor.columnNames) { tempData[media] = helper.chooseWithFilterType( withType, @@ -101,10 +100,10 @@ class OnWithFiltersQuery : ViewModel() { ) } - // If [withType] is a song media, add the extra information. + // If 'withType' is a song media, add the extra information. if (withType == MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) { //Get a extra information from audio, e.g: extension, uri, etc.. - val tempExtraData = helper.loadSongExtraInfo(uri, tempData) + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) tempData.putAll(tempExtraData) } @@ -113,8 +112,6 @@ class OnWithFiltersQuery : ViewModel() { // Close cursor to avoid memory leaks. cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). return@withContext withFiltersList } } \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/helper/OnAudioHelper.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt similarity index 99% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/helper/OnAudioHelper.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt index 0584b93b..083c7df3 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/helper/OnAudioHelper.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt @@ -1,4 +1,4 @@ -package com.lucasjosino.on_audio_query.query.helper +package com.lucasjosino.on_audio_query.queries.helper import android.content.ContentResolver import android.content.ContentUris @@ -9,7 +9,7 @@ import android.provider.MediaStore import android.util.Log import java.io.File -class OnAudioHelper { +class QueryHelper { //This method will load some extra information about audio/song fun loadSongExtraInfo( uri: Uri, diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAllPathQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAllPathQuery.kt deleted file mode 100644 index 3b905b69..00000000 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAllPathQuery.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.lucasjosino.on_audio_query.query - -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import android.provider.MediaStore -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import io.flutter.plugin.common.MethodChannel -import java.io.File - -/** OnAllPathQuery */ -class OnAllPathQuery { - - // Main parameters, none of this methods can be null. - private val uri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - private lateinit var resolver: ContentResolver - - /** - * Method to "query" all paths. - * - * Parameters: - * * [context] - * * [result] - */ - fun queryAllPath(context: Context, result: MethodChannel.Result) { - this.resolver = context.contentResolver - - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultAllPath = ArrayList() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultAllPath = loadAllPath() - } - - // Send to Dart. - result.success(resultAllPath) - } - - // Ignore the [Data] deprecation because this plugin support older versions. - @Suppress("DEPRECATION") - private fun loadAllPath(): ArrayList { - // Setup the cursor with [uri]. - val cursor = resolver.query(uri, null, null, null, null) - // Empty list. - val songPathList: ArrayList = ArrayList() - - // For each item(path) inside this "cursor", take one and add to the list. - while (cursor != null && cursor.moveToNext()) { - val content = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)) - val path = File(content).parent - // Check if path is null or if already exist inside list. - if (path != null && !songPathList.contains(path)) songPathList.add(path) - } - - // Close cursor to avoid memory leaks. - cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). - return songPathList - } -} \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosQuery.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosQuery.kt deleted file mode 100644 index f41f21b0..00000000 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/query/OnAudiosQuery.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.lucasjosino.on_audio_query.query - -import android.annotation.SuppressLint -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.lucasjosino.on_audio_query.OnAudioQueryPlugin -import com.lucasjosino.on_audio_query.query.helper.OnAudioHelper -import com.lucasjosino.on_audio_query.types.checkAudiosUriType -import com.lucasjosino.on_audio_query.types.sorttypes.checkSongSortType -import com.lucasjosino.on_audio_query.utils.songProjection -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** OnAudiosQuery */ -class OnAudiosQuery : ViewModel() { - - // Main parameters - private val helper = OnAudioHelper() - private var selection: String? = null - - // None of this methods can be null. - private lateinit var uri: Uri - private lateinit var resolver: ContentResolver - private lateinit var sortType: String - - @SuppressLint("StaticFieldLeak") - private lateinit var context: Context - - - /** - * Method to "query" all songs. - * - * Parameters: - * * [context] - * * [result] - * * [call] - */ - // Ignore the [Data] deprecation because this plugin support older versions. - @Suppress("DEPRECATION") - fun querySongs( - context: Context, - result: MethodChannel.Result, - call: MethodCall - ) { - this.context = context; resolver = context.contentResolver - - // Sort: Type and Order. - sortType = checkSongSortType( - call.argument("sortType"), - call.argument("orderType")!!, - call.argument("ignoreCase")!! - ) - // Check uri: - // * [0]: External. - // * [1]: Internal. - uri = checkAudiosUriType(call.argument("uri")!!) - // Here we provide a custom 'path'. - if (call.argument("path") != null) { - val projection = songProjection() - selection = projection[0] + " like " + "'%" + call.argument("path") + "/%'" - } - - // Query everything in background for a better performance. - viewModelScope.launch { - // Request permission status from the main method. - val hasPermission = OnAudioQueryPlugin().onPermissionStatus(context) - // Empty list. - var resultSongList = ArrayList>() - - // We cannot "query" without permission so, just return a empty list. - if (hasPermission) { - // Start querying - resultSongList = loadSongs() - } - - //Flutter UI will start, but, information still loading - result.success(resultSongList) - } - } - - //Loading in Background - private suspend fun loadSongs(): ArrayList> = - withContext(Dispatchers.IO) { - - // Setup the cursor with [uri], [projection] and [sortType]. - val cursor = resolver.query(uri, songProjection(), selection, null, sortType) - // Empty list. - val songList: ArrayList> = ArrayList() - - // For each item(song) inside this "cursor", take one and "format" - // into a [Map]. - while (cursor != null && cursor.moveToNext()) { - val tempData: MutableMap = HashMap() - for (audioMedia in cursor.columnNames) { - tempData[audioMedia] = helper.loadSongItem(audioMedia, cursor) - } - - //Get a extra information from audio, e.g: extension, uri, etc.. - val tempExtraData = helper.loadSongExtraInfo(uri, tempData) - tempData.putAll(tempExtraData) - - songList.add(tempData) - } - - // Close cursor to avoid memory leaks. - cursor?.close() - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). - return@withContext songList - } -} - -//Extras: - -// * Query only audio > 60000 ms [1 minute] -// Obs: I don't think is a good idea, some audio "Non music" have more than 1 minute -//query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, MediaStore.Audio.Media.DURATION + -// ">= 60000", null, checkSongSortType(sortType!!)) - -// * Query audio with limit, used for better performance in tests -//MediaStore.Audio.Media.TITLE + " LIMIT 4" - -// * All projection types in android [Audio] -//I/AudioCursor[All]: [ -// title_key, -// instance_id, -// duration, -// is_ringtone, -// album_artist, -// orientation, -// artist, -// height, -// is_drm, -// bucket_display_name, -// is_audiobook, -// owner_package_name, -// volume_name, -// title_resource_uri, -// date_modified, -// date_expires, -// composer, -// _display_name, -// datetaken, -// mime_type, -// is_notification, -// _id, -// year, -// _data, -// _hash, -// _size, -// album, -// is_alarm, -// title, -// track, -// width, -// is_music, -// album_key, -// is_trashed, -// group_id, -// document_id, -// artist_id, -// artist_key, -// is_pending, -// date_added, -// is_podcast, -// album_id, -// primary_directory, -// secondary_directory, -// original_document_id, -// bucket_id, -// bookmark, -// relative_path -// ] \ No newline at end of file diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt index e898834e..024ba082 100644 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt +++ b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt @@ -6,7 +6,7 @@ import android.provider.MediaStore fun checkArtworkType(sortType: Int): Uri { return when (sortType) { - 0,2,3, 4 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + 0, 2, 3, 4 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 1 -> MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI else -> throw Exception("[checkArtworkType] value don't exist!") } diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnCursorProjections.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/CursorProjection.kt similarity index 100% rename from on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnCursorProjections.kt rename to on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/CursorProjection.kt diff --git a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnDeviceInfo.kt b/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnDeviceInfo.kt deleted file mode 100644 index 72372ee1..00000000 --- a/on_audio_query/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/OnDeviceInfo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.lucasjosino.on_audio_query.utils - -import android.os.Build -import io.flutter.plugin.common.MethodChannel - -fun queryDeviceInfo(result: MethodChannel.Result) { - val deviceData: MutableMap = HashMap() - deviceData["device_model"] = Build.MODEL - deviceData["device_sys_version"] = Build.VERSION.SDK_INT - deviceData["device_sys_type"] = "Android" - result.success(deviceData) -} \ No newline at end of file diff --git a/on_audio_query/example/android/app/src/main/AndroidManifest.xml b/on_audio_query/example/android/app/src/main/AndroidManifest.xml index 215d9df3..183e75cd 100644 --- a/on_audio_query/example/android/app/src/main/AndroidManifest.xml +++ b/on_audio_query/example/android/app/src/main/AndroidManifest.xml @@ -3,13 +3,14 @@ + - - - - + + + + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 308D7FDF12FC5F39971BB53B /* [CP] Embed Pods Frameworks */ = { + 9099D3364DFEDF1A637FD9DD /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -236,20 +250,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -363,7 +363,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExample; + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -493,7 +493,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExample; + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -517,7 +517,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExample; + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/on_audio_query/example/ios/Runner/Info.plist b/on_audio_query/example/ios/Runner/Info.plist index 05be961e..c9fdbedb 100644 --- a/on_audio_query/example/ios/Runner/Info.plist +++ b/on_audio_query/example/ios/Runner/Info.plist @@ -22,6 +22,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppleMusicUsageDescription + Please let this application access your music library UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,7 +43,5 @@ UIViewControllerBasedStatusBarAppearance - NSAppleMusicUsageDescription - Please let this application access your music library diff --git a/on_audio_query/example/lib/main.dart b/on_audio_query/example/lib/main.dart index 43387f49..22247ae3 100644 --- a/on_audio_query/example/lib/main.dart +++ b/on_audio_query/example/lib/main.dart @@ -14,7 +14,6 @@ Copyright: © 2021, Lucas Josino. All rights reserved. import 'package:flutter/material.dart'; import 'package:on_audio_query/on_audio_query.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; void main() { runApp( @@ -32,23 +31,36 @@ class Songs extends StatefulWidget { } class _SongsState extends State { + // Main method. final OnAudioQuery _audioQuery = OnAudioQuery(); + // Indicate if application has permission to the library. + bool _hasPermission = false; + @override void initState() { super.initState(); - requestPermission(); + // (Optinal) Set logging level. By default will be set to 'WARN'. + // + // Log will appear on: + // * XCode: Debug Console + // * VsCode: Debug Console + // * Android Studio: Debug and Logcat Console + LogConfig logConfig = LogConfig(logType: LogType.DEBUG); + _audioQuery.setLogConfig(logConfig); + + // Check and request for permission. + checkAndRequestPermissions(); } - requestPermission() async { - // Web platform don't support permissions methods. - if (!kIsWeb) { - bool permissionStatus = await _audioQuery.permissionsStatus(); - if (!permissionStatus) { - await _audioQuery.permissionsRequest(); - } - setState(() {}); - } + checkAndRequestPermissions({bool retry = false}) async { + // The param 'retryRequest' is false, by default. + _hasPermission = await _audioQuery.checkAndRequest( + retryRequest: retry, + ); + + // Only call update the UI if application has all required permissions. + _hasPermission ? setState(() {}) : null; } @override @@ -58,41 +70,73 @@ class _SongsState extends State { title: const Text("OnAudioQueryExample"), elevation: 2, ), - body: FutureBuilder>( - // Default values: - future: _audioQuery.querySongs( - sortType: null, - orderType: OrderType.ASC_OR_SMALLER, - uriType: UriType.EXTERNAL, - ignoreCase: true, - ), - builder: (context, item) { - // Loading content - if (item.data == null) return const CircularProgressIndicator(); + body: Center( + child: !_hasPermission + ? noAccessToLibraryWidget() + : FutureBuilder>( + // Default values: + future: _audioQuery.querySongs( + sortType: null, + orderType: OrderType.ASC_OR_SMALLER, + uriType: UriType.EXTERNAL, + ignoreCase: true, + ), + builder: (context, item) { + // Display error, if any. + if (item.hasError) { + return Text(item.error.toString()); + } - // When you try "query" without asking for [READ] or [Library] permission - // the plugin will return a [Empty] list. - if (item.data!.isEmpty) return const Text("Nothing found!"); + // Waiting content. + if (item.data == null) { + return const CircularProgressIndicator(); + } - // You can use [item.data!] direct or you can create a: - // List songs = item.data!; - return ListView.builder( - itemCount: item.data!.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(item.data![index].title), - subtitle: Text(item.data![index].artist ?? "No Artist"), - trailing: const Icon(Icons.arrow_forward_rounded), - // This Widget will query/load image. Just add the id and type. - // You can use/create your own widget/method using [queryArtwork]. - leading: QueryArtworkWidget( - id: item.data![index].id, - type: ArtworkType.AUDIO, - ), - ); - }, - ); - }, + // 'Library' is empty. + if (item.data!.isEmpty) return const Text("Nothing found!"); + + // You can use [item.data!] direct or you can create a: + // List songs = item.data!; + return ListView.builder( + itemCount: item.data!.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(item.data![index].title), + subtitle: Text(item.data![index].artist ?? "No Artist"), + trailing: const Icon(Icons.arrow_forward_rounded), + // This Widget will query/load image. + // You can use/create your own widget/method using [queryArtwork]. + leading: QueryArtworkWidget( + controller: _audioQuery, + id: item.data![index].id, + type: ArtworkType.AUDIO, + ), + ); + }, + ); + }, + ), + ), + ); + } + + Widget noAccessToLibraryWidget() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.redAccent.withOpacity(0.5), + ), + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Application doesn't have access to the library"), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () => checkAndRequestPermissions(retry: true), + child: const Text("Allow"), + ), + ], ), ); } diff --git a/on_audio_query/example/pubspec.yaml b/on_audio_query/example/pubspec.yaml index 8b2f107c..9576001a 100644 --- a/on_audio_query/example/pubspec.yaml +++ b/on_audio_query/example/pubspec.yaml @@ -43,8 +43,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - assets: - - assets/ + # assets: + # - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/on_audio_query/ios/Classes/PluginProvider.swift b/on_audio_query/ios/Classes/PluginProvider.swift new file mode 100644 index 00000000..9afa6033 --- /dev/null +++ b/on_audio_query/ios/Classes/PluginProvider.swift @@ -0,0 +1,46 @@ +/** + * A singleton used to define all variables/methods that will be used on all plugin. + * + * The singleton will provider the ability to 'request' required variables/methods on any moment. + * + * All variables/methods should be defined after dart request (call/result). + */ +enum PluginProvider { + private static let ERROR_MESSAGE = "Tried to get one of the methods but the 'PluginProvider' has not been initialized" + + private static var _call: FlutterMethodCall? + + private static var _result: FlutterResult? + + /** + * Used to define the current dart request. + * + * Should be defined/redefined on every [handle] request. + */ + static func set(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + PluginProvider._call = call + PluginProvider._result = result + } + + static func call() throws -> FlutterMethodCall { + Log.type.debug(_call == nil) + guard _call != nil else { + throw PluginProviderException.unitialized(ERROR_MESSAGE) + } + + return _call! + } + + static func result() throws -> FlutterResult { + Log.type.debug(_call == nil) + guard _result != nil else { + throw PluginProviderException.unitialized(ERROR_MESSAGE) + } + + return _result! + } + + enum PluginProviderException: Error { + case unitialized(String) + } +} diff --git a/on_audio_query/ios/Classes/SwiftOnAudioQueryPlugin.swift b/on_audio_query/ios/Classes/SwiftOnAudioQueryPlugin.swift index 449b4bed..f19ee95d 100644 --- a/on_audio_query/ios/Classes/SwiftOnAudioQueryPlugin.swift +++ b/on_audio_query/ios/Classes/SwiftOnAudioQueryPlugin.swift @@ -1,55 +1,85 @@ import Flutter import UIKit -import MediaPlayer public class SwiftOnAudioQueryPlugin: NSObject, FlutterPlugin { + private static let CHANNEL_NAME: String = "com.lucasjosino.on_audio_query" + + override public init() { + Log.setLogLevel(level: .warning) + } // Dart <-> Swift communication. public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.lucasjosino.on_audio_query", binaryMessenger: registrar.messenger()) + Log.type.info("Called register") + + let channel = FlutterMethodChannel( + name: "\(CHANNEL_NAME)", + binaryMessenger: registrar.messenger() + ) let instance = SwiftOnAudioQueryPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + Log.type.debug("Started method call (\(call.method))") + + // Init the plugin provider with current 'call' and 'result'. + PluginProvider.set(call, result) + + Log.type.info("Method call: \(call.method)") switch call.method { // This is a basic permission handler, will return [true] if has permission and // [false] if don't. // // The others status will be ignored and replaced with [false]. - case "permissionsStatus": - result(checkPermission()) + case Method.PERMISSION_STATUS: + result(PermissionController.checkPermission()) // The same as [permissionStatus], this is a basic permission handler and will only // return [true] or [false]. // // When adding the necessary [permissions] inside [Info.plist], [IOS] will automatically // request but, in any case, you can call this method. - case "permissionsRequest": - MPMediaLibrary.requestAuthorization { status in - if (status == .authorized) { - result(true) - } else { - result(false) - } - } + case Method.PERMISSION_REQUEST: + result(PermissionController.requestPermission()) // Some basic information about the platform, in this case, [IOS] // * Model (Only return the "type", like: IPhone, MacOs, IPod..) // * Version (IOS version) // * Type (IOS) - case "queryDeviceInfo": - queryDeviceInfo(result: result) + case Method.QUERY_DEVICE_INFO: + let device = UIDevice.current + result([ + "device_model": device.model, + "device_sys_type": device.systemName, + "device_sys_version": device.systemVersion, + ]) + // Set logging level. + case Method.SET_LOG_CONFIG: + let args = call.arguments as! [String: Any] + let level = args["level"] as! Int + Log.setLogLevel(dartLevel: level) + result(true) default: - // - OnAudioController(call: call, result: result).chooseMethod() - } - } - - public func checkPermission() -> Bool { - let permissionStatus = MPMediaLibrary.authorizationStatus() - if permissionStatus == MPMediaLibraryAuthorizationStatus.authorized { - return true - } else { - return false + Log.type.debug("Checking permissions...") + + let hasPermission = PermissionController.checkPermission() + Log.type.debug("Application has permissions: \(hasPermission)") + + if !hasPermission { + Log.type.error("The application doesn't have access to the library") + result( + FlutterError( + code: "MissingPermissions", + message: "Application doesn't have access to the library", + details: "Call the [permissionsRequest] method or install a external plugin to handle the app permission." + ) + ) + break + } + + MethodController().find() } + + Log.type.debug("Ended method call (\(call.method))") } } diff --git a/on_audio_query/ios/Classes/consts/Method.swift b/on_audio_query/ios/Classes/consts/Method.swift new file mode 100644 index 00000000..b78a1fb8 --- /dev/null +++ b/on_audio_query/ios/Classes/consts/Method.swift @@ -0,0 +1,27 @@ +struct Method { + // General methods + static let PERMISSION_STATUS = "permissionsStatus" + static let PERMISSION_REQUEST = "permissionsRequest" + static let QUERY_DEVICE_INFO = "queryDeviceInfo" + static let SCAN = "scan" + static let SET_LOG_CONFIG = "setLogConfig" + + // Query methods + static let QUERY_AUDIOS = "querySongs" + static let QUERY_ALBUMS = "queryAlbums" + static let QUERY_ARTISTS = "queryArtists" + static let QUERY_GENRES = "queryGenres" + static let QUERY_PLAYLISTS = "queryPlaylists" + static let QUERY_ARTWORK = "queryArtwork" + static let QUERY_AUDIOS_FROM = "queryAudiosFrom" + static let QUERY_WITH_FILTERS = "queryWithFilters" + static let QUERY_ALL_PATHS = "queryAllPath" + + // Playlist methods + static let CREATE_PLAYLIST = "createPlaylist" + static let REMOVE_PLAYLIST = "removePlaylist" + static let ADD_TO_PLAYLIST = "addToPlaylist" + static let REMOVE_FROM_PLAYLIST = "removeFromPlaylist" + static let RENAME_PLAYLIST = "renamePlaylist" + static let MOVE_ITEM_TO = "moveItemTo" +} diff --git a/on_audio_query/ios/Classes/controller/OnAudioController.swift b/on_audio_query/ios/Classes/controller/OnAudioController.swift deleted file mode 100644 index 0c728e3a..00000000 --- a/on_audio_query/ios/Classes/controller/OnAudioController.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Flutter - -public class OnAudioController { - var call: FlutterMethodCall - var result: FlutterResult - - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - self.call = call - self.result = result - } - - - //This method will sort call according to request. - public func chooseMethod() { - // All necessary method to this plugin support both platforms, only playlists - // are limited when using [IOS]. - switch call.method { - case "querySongs": - OnAudioQuery(call: call, result: result).querySongs() - case "queryAlbums": - OnAlbumsQuery(call: call, result: result).queryAlbums() - case "queryArtists": - OnArtistsQuery(call: call, result: result).queryArtists() - case "queryGenres": - OnGenresQuery(call: call, result: result).queryGenres() - case "queryPlaylists": - OnPlaylistsQuery(call: call, result: result).queryPlaylists() - case "queryAudiosFrom": - OnAudiosFromQuery(call: call, result: result).queryAudiosFrom() - case "queryWithFilters": - OnWithFiltersQuery(call: call, result: result).queryWithFilters() - case "queryArtwork": - OnArtworkQuery(call: call, result: result).queryArtwork() - // The playlist for [IOS] is completely limited, the developer can only: - // * Create playlist - // * Add item to playlist (Unsuported, for now) - // - // Missing methods: - // * Rename playlist - // * Remove playlist - // * Remove item from playlist - // * Move item inside playlist - case "createPlaylist": - OnPlaylistsController(call: call, result: result).createPlaylist() - case "addToPlaylist": - OnPlaylistsController(call: call, result: result).addToPlaylist() - default: - // All non suported methods will throw this error. - result(FlutterMethodNotImplemented) - } - } -} diff --git a/on_audio_query/ios/Classes/controller/OnPlaylistsController.swift b/on_audio_query/ios/Classes/controller/OnPlaylistsController.swift deleted file mode 100644 index d99ea104..00000000 --- a/on_audio_query/ios/Classes/controller/OnPlaylistsController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Flutter -import MediaPlayer - -class OnPlaylistsController { - var args: [String: Any] - var result: FlutterResult - - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - self.args = call.arguments as! [String: Any] - self.result = result - } - - // Due the [IOS] limitation, for now we can only create/add to playlists. - func createPlaylist() { - // The name, author and description for playlist, playlistName cannot be null. - let playlistName = args["playlistName"] as! String - let playlistAuthor = args["playlistAuthor"] as? String - let playlistDesc = args["playlistDesc"] as? String - - // - let playlistMetadata = MPMediaPlaylistCreationMetadata.init(name: playlistName) - playlistMetadata.authorDisplayName = playlistAuthor - playlistMetadata.descriptionText = playlistDesc ?? "" - - // - MPMediaLibrary().getPlaylist(with: UUID.init(), creationMetadata: playlistMetadata, completionHandler: { playlist, error in - //A little second to create the playlist and Flutter UI update - sleep(1) - if playlist != nil { - self.result(true) - } else { - print(error ?? "Something wrong happend") - self.result(false) - } - } - ) - } - - func addToPlaylist() { - // - let playlistId = args["playlistId"] as! Int - let audioId = args["audioId"] as! Int - - // TODO: Use another method to get UUID from playlist - // - // Link: https://github.com/HumApp/MusicKit/blob/master/AppleMusicSample/Controllers/MediaLibraryManager.swift - let playlist: MPMediaPlaylist? = loadPlaylist(id: playlistId) - - // [addItem] won't work in the main thread. - DispatchQueue.global(qos: .userInitiated).async { - var hasAdded: Bool = false - - // If playlist is null, just return [false]. - if playlist != nil { - playlist!.addItem(withProductID: String(audioId), completionHandler: { error in - if error == nil { - hasAdded = true - } else { - hasAdded = false - // TODO: Fix "NSLocalizedDescription=The requested operation is not enabled for this device." - print("on_audio_error: " + error.debugDescription) - } - }) - } else { - hasAdded = false - } - - DispatchQueue.main.async { - self.result(hasAdded) - } - } - } - - private func loadPlaylist(id: Int) -> MPMediaPlaylist? { - let cursor = MPMediaQuery.playlists() - - // - let playlistFilter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) - let noCloudItemFilter = MPMediaPropertyPredicate.init(value: false, forProperty: MPMediaItemPropertyIsCloudItem) - cursor.addFilterPredicate(playlistFilter) - cursor.addFilterPredicate(noCloudItemFilter) - - let firstPlaylist = cursor.collections?.first as? MPMediaPlaylist - return firstPlaylist - } -} diff --git a/on_audio_query/ios/Classes/controllers/MethodController.swift b/on_audio_query/ios/Classes/controllers/MethodController.swift new file mode 100644 index 00000000..fce2543d --- /dev/null +++ b/on_audio_query/ios/Classes/controllers/MethodController.swift @@ -0,0 +1,45 @@ +import Flutter + +public class MethodController { + public func find() { + let call = try! PluginProvider.call() + let result = try! PluginProvider.result() + + // All necessary method to this plugin support both platforms, only playlists + // are limited when using [IOS]. + switch call.method { + case Method.QUERY_AUDIOS: + AudioQuery().querySongs() + case Method.QUERY_ALBUMS: + AlbumQuery().queryAlbums() + case Method.QUERY_ARTISTS: + ArtistQuery().queryArtists() + case Method.QUERY_GENRES: + GenreQuery().queryGenres() + case Method.QUERY_PLAYLISTS: + PlaylistQuery().queryPlaylists() + case Method.QUERY_AUDIOS_FROM: + AudioFromQuery().queryAudiosFrom() + case Method.QUERY_WITH_FILTERS: + WithFiltersQuery().queryWithFilters() + case Method.QUERY_ARTWORK: + ArtworkQuery().queryArtwork() + // The playlist for [IOS] is completely limited, the developer can only: + // * Create playlist + // * Add item to playlist (Unsuported, for now) + // + // Missing methods: + // * Rename playlist + // * Remove playlist + // * Remove item from playlist + // * Move item inside playlist + case Method.CREATE_PLAYLIST: + PlaylistController().createPlaylist() + case Method.ADD_TO_PLAYLIST: + PlaylistController().addToPlaylist() + default: + // All non suported methods will throw this error. + result(FlutterMethodNotImplemented) + } + } +} diff --git a/on_audio_query/ios/Classes/controllers/PermissionController.swift b/on_audio_query/ios/Classes/controllers/PermissionController.swift new file mode 100644 index 00000000..37b145a2 --- /dev/null +++ b/on_audio_query/ios/Classes/controllers/PermissionController.swift @@ -0,0 +1,25 @@ +import MediaPlayer + +class PermissionController { + public static func checkPermission() -> Bool { + let permissionStatus = MPMediaLibrary.authorizationStatus() + if permissionStatus == MPMediaLibraryAuthorizationStatus.authorized { + return true + } else { + return false + } + } + + public static func requestPermission() -> Bool { + Log.type.debug("Requesting permissions.") + Log.type.debug("iOS Version: \(ProcessInfo().operatingSystemVersion.majorVersion)") + + var isPermissionGranted: Bool = false + MPMediaLibrary.requestAuthorization { status in + isPermissionGranted = status == .authorized + Log.type.debug("Permission accepted: \(isPermissionGranted)") + } + + return isPermissionGranted + } +} diff --git a/on_audio_query/ios/Classes/controllers/PlaylistController.swift b/on_audio_query/ios/Classes/controllers/PlaylistController.swift new file mode 100644 index 00000000..5fbd4269 --- /dev/null +++ b/on_audio_query/ios/Classes/controllers/PlaylistController.swift @@ -0,0 +1,89 @@ +import Flutter +import MediaPlayer + +class PlaylistController { + var args: [String: Any] + var result: FlutterResult + + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() + } + + func createPlaylist() { + let playlistName = args["playlistName"] as! String + let playlistAuthor = args["playlistAuthor"] as? String + let playlistDesc = args["playlistDesc"] as? String + + Log.type.debug("Playlist info: ") + Log.type.debug("\tname: \(playlistName)") + Log.type.debug("\tauthor: \(String(describing: playlistAuthor))") + Log.type.debug("\tdescription: \(String(describing: playlistDesc))") + + let playlistMetadata = MPMediaPlaylistCreationMetadata(name: playlistName) + playlistMetadata.authorDisplayName = playlistAuthor + playlistMetadata.descriptionText = playlistDesc ?? "" + + MPMediaLibrary().getPlaylist(with: UUID(), creationMetadata: playlistMetadata, completionHandler: { playlist, _ in + sleep(1) + + let playlistHasBeenCreated = playlist != nil + Log.type.debug("Playlist has been created: \(playlistHasBeenCreated)") + + self.result(playlistHasBeenCreated) + }) + } + + func addToPlaylist() { + let playlistId = args["playlistId"] as! Int + let audioId = args["audioId"] as! Int + + Log.type.debug("Playlist info: ") + Log.type.debug("\tid: \(playlistId)") + Log.type.debug("\taudioId: \(audioId)") + + // TODO: Use another method to get UUID from playlist + // + // Link: https://github.com/HumApp/MusicKit/blob/master/AppleMusicSample/Controllers/MediaLibraryManager.swift + let playlist: MPMediaPlaylist? = loadPlaylist(id: playlistId) + + // [addItem] won't work in the main thread. + DispatchQueue.global(qos: .userInitiated).async { + var hasBeenAdded = false + + if playlist != nil { + Log.type.debug("Found playlist! Name: \(playlist?.name ?? "Unknown")") + + playlist!.addItem(withProductID: String(audioId), completionHandler: { error in + let hasError = error != nil + + if hasError { + Log.type.error(error.debugDescription) + } + + hasBeenAdded = !hasError + }) + } + + DispatchQueue.main.async { + Log.type.debug("Item (\(audioId)) has been added: \(hasBeenAdded)") + self.result(hasBeenAdded) + } + } + } + + private func loadPlaylist(id: Int) -> MPMediaPlaylist? { + let cursor = MPMediaQuery.playlists() + + // Create a filter using the playlist id. + let playlistFilter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) + + // Remove any cloud playlist. + let noCloudItemFilter = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem) + + cursor.addFilterPredicate(playlistFilter) + cursor.addFilterPredicate(noCloudItemFilter) + + return cursor.collections?.first as? MPMediaPlaylist + } +} diff --git a/on_audio_query/ios/Classes/queries/AlbumQuery.swift b/on_audio_query/ios/Classes/queries/AlbumQuery.swift new file mode 100644 index 00000000..82c45b55 --- /dev/null +++ b/on_audio_query/ios/Classes/queries/AlbumQuery.swift @@ -0,0 +1,58 @@ +import Flutter +import MediaPlayer + +class AlbumQuery { + var args: [String: Any] + var result: FlutterResult + + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() + } + + func queryAlbums() { + let sortType = args["sortType"] as? Int ?? 0 + + let cursor = MPMediaQuery.albums() + + // Using native sort from [IOS] you can only use the [Album] and [Artist]. + // The others will be sorted "manually" using [formatAlbumList] before + // send to Dart. + cursor.groupingType = checkSongSortType(sortType: sortType) + + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor.addFilterPredicate(cloudFilter) + + Log.type.debug("Query config: ") + Log.type.debug("\tsortType: \(sortType)") + + // Query everything in background for a better performance. + loadAlbums(cursor: cursor.collections) + } + + private func loadAlbums(cursor: [MPMediaItemCollection]!) { + DispatchQueue.global(qos: .userInitiated).async { + var listOfAlbums: [[String: Any?]] = Array() + + // For each item(album) inside this "cursor", take one and "format" + // into a [Map], all keys are based on [Android] + // platforms. + for album in cursor { + if !album.items[0].isCloudItem, album.items[0].assetURL != nil { + let albumData = loadAlbumItem(album: album) + listOfAlbums.append(albumData) + } + } + + DispatchQueue.main.async { + // Custom sort/order. + let finalList = formatAlbumList(args: self.args, allAlbums: listOfAlbums) + self.result(finalList) + } + } + } +} diff --git a/on_audio_query/ios/Classes/queries/ArtistQuery.swift b/on_audio_query/ios/Classes/queries/ArtistQuery.swift new file mode 100644 index 00000000..b1f43d5b --- /dev/null +++ b/on_audio_query/ios/Classes/queries/ArtistQuery.swift @@ -0,0 +1,51 @@ +import Flutter +import MediaPlayer + +class ArtistQuery { + var args: [String: Any] + var result: FlutterResult + + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() + } + + func queryArtists() { + let cursor = MPMediaQuery.artists() + + // We don't need to define a sortType here. [IOS] only support + // the [Artist]. The others will be sorted "manually" using + // [formatSongList] before send to Dart. + + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor.addFilterPredicate(cloudFilter) + + loadArtists(cursor: cursor.collections) + } + + private func loadArtists(cursor: [MPMediaItemCollection]!) { + DispatchQueue.global(qos: .userInitiated).async { + var listOfArtists: [[String: Any?]] = Array() + + // For each item(artist) inside this "cursor", take one and "format" + // into a [Map], all keys are based on [Android] + // platforms. + for artist in cursor { + if !artist.items[0].isCloudItem, artist.items[0].assetURL != nil { + let artistData = loadArtistItem(artist: artist) + listOfArtists.append(artistData) + } + } + + DispatchQueue.main.async { + // Custom sort/order. + let finalList = formatArtistList(args: self.args, allArtists: listOfArtists) + self.result(finalList) + } + } + } +} diff --git a/on_audio_query/ios/Classes/queries/ArtworkQuery.swift b/on_audio_query/ios/Classes/queries/ArtworkQuery.swift new file mode 100644 index 00000000..be56f6a8 --- /dev/null +++ b/on_audio_query/ios/Classes/queries/ArtworkQuery.swift @@ -0,0 +1,127 @@ +import Flutter +import MediaPlayer + +class ArtworkQuery { + var args: [String: Any] + var result: FlutterResult + + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() + } + + // [IOS] has a different artwork system and you can "query" using normal "querySongs, .." + // [Android] can't "query" artwork at the same time as "querySongs", so we need to "query" + // using a different method(queryArtwork). + // + // To match both [IOS] and [Android], [queryArtwork] is the only way to get artwork. + // + // Not the best solution but, at least here we can select differents formats and size. + func queryArtwork() { + // The 'id' of the [Song] or [Album]. + let id = args["id"] as! Int + + // The 'size' of the image. + let size = args["size"] as! Int + + // The 'size' of the image. + var quality = args["quality"] as! Int + if quality > 100 { + quality = 50 + } + + // The format + // * 0 -> JPEG + // * 1 -> PNG + let format = args["format"] as! Int + + var cursor: MPMediaQuery? + var filter: MPMediaPropertyPredicate? + + let uri = args["type"] as! Int + switch uri { + case 0: + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyPersistentID) + cursor = MPMediaQuery.songs() + case 1: + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyAlbumPersistentID) + cursor = MPMediaQuery.albums() + case 2: + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) + cursor = MPMediaQuery.playlists() + case 3: + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyArtistPersistentID) + cursor = MPMediaQuery.artists() + case 4: + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyGenrePersistentID) + cursor = MPMediaQuery.genres() + default: + filter = nil + cursor = nil + } + + if cursor == nil || filter == nil { + Log.type.warning("Cursor or filter has null value!") + result(nil) + return + } + + Log.type.debug("Query config: ") + Log.type.debug("\tid: \(id)") + Log.type.debug("\tsize: \(size)") + Log.type.debug("\tquality: \(quality)") + Log.type.debug("\tformat: \(format)") + Log.type.debug("\turi: \(uri)") + Log.type.debug("\tfilter: \(String(describing: filter))") + + cursor!.addFilterPredicate(filter!) + + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor?.addFilterPredicate(cloudFilter) + + // Query everything in background for a better performance. + loadArtwork(cursor: cursor, size: size, format: format, uri: uri, quality: quality) + } + + private func loadArtwork(cursor: MPMediaQuery!, size: Int, format: Int, uri: Int, quality: Int) { + DispatchQueue.global(qos: .userInitiated).async { + var artwork: Data? + var item: MPMediaItem? + let convertedQuality = CGFloat(Double(quality) / 100.0) + + // 'uri' == 0 -> artwork is from [Song] + // 'uri' == 1, 2 or 3 -> artwork is from [Album], [Playlist] or [Artist] + if uri == 0 { + item = cursor!.items?.first + } else { + item = cursor!.collections?.first?.items[0] + } + + let cgSize = CGSize(width: size, height: size) + let image: UIImage? = item?.artwork?.image(at: cgSize) + + // 'format' == 0 -> JPEG + // 'format' == 1 -> PNG + if format == 0 { + artwork = image?.jpegData(compressionQuality: convertedQuality) + } else { + // [PNG] format will return a high quality image. + artwork = image?.pngData() + } + + DispatchQueue.main.async { + // Avoid "empty" or broken image. + if artwork != nil, artwork!.isEmpty { + Log.type.info("Item (\(item?.persistentID ?? 0)) has a null or empty artwork") + artwork = nil + } + + self.result(artwork) + } + } + } +} diff --git a/on_audio_query/ios/Classes/query/OnAudiosFromQuery.swift b/on_audio_query/ios/Classes/queries/AudioFromQuery.swift similarity index 63% rename from on_audio_query/ios/Classes/query/OnAudiosFromQuery.swift rename to on_audio_query/ios/Classes/queries/AudioFromQuery.swift index ba8139eb..8b4aa069 100644 --- a/on_audio_query/ios/Classes/query/OnAudiosFromQuery.swift +++ b/on_audio_query/ios/Classes/queries/AudioFromQuery.swift @@ -1,20 +1,19 @@ import Flutter import MediaPlayer -class OnAudiosFromQuery { +class AudioFromQuery { var args: [String: Any] var result: FlutterResult var type: Int = -1 - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() } func queryAudiosFrom() { // - self.type = args["type"] as! Int + type = args["type"] as! Int let wh3re = args["where"] as Any // The sortType. let sortType = args["sortType"] as? Int ?? 0 @@ -27,37 +26,29 @@ class OnAudiosFromQuery { // send to Dart. cursor?.groupingType = checkSongSortType(sortType: sortType) - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Here we'll check if the request is to [Playlist] or other. - if self.type != 6 && cursor != nil { + // Here we'll check if the request is to [Playlist] or other. + if type != 6, cursor != nil { + // This filter will avoid audios/songs outside phone library(cloud). + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor?.addFilterPredicate(cloudFilter) - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor?.addFilterPredicate(cloudFilter) - - // Query everything in background for a better performance. - loadQueryAudiosFrom(cursor: cursor!) - } else { - // Query everything in background for a better performance. - cursor = MPMediaQuery.playlists() + // Query everything in background for a better performance. + loadQueryAudiosFrom(cursor: cursor!) + } else { + // Query everything in background for a better performance. + cursor = MPMediaQuery.playlists() - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor?.addFilterPredicate(cloudFilter) + // This filter will avoid audios/songs outside phone library(cloud). + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor?.addFilterPredicate(cloudFilter) - loadSongsFromPlaylist(cursor: cursor!.collections) - } - } else { - // There's no permission so, return empty to avoid crashes. - result([]) + loadSongsFromPlaylist(cursor: cursor!.collections) } } @@ -70,7 +61,7 @@ class OnAudiosFromQuery { // platforms so, if you change some key, will have to change the [Android] too. for song in cursor.items! { // If the song file don't has a assetURL, is a Cloud item. - if !song.isCloudItem && song.assetURL != nil { + if !song.isCloudItem, song.assetURL != nil { let songData = loadSongItem(song: song) listOfSongs.append(songData) } @@ -86,7 +77,7 @@ class OnAudiosFromQuery { } } - //Add a better code + // Add a better code private func loadSongsFromPlaylist(cursor: [MPMediaItemCollection]!) { DispatchQueue.global(qos: .userInitiated).async { var listOfSongs: [[String: Any?]] = Array() @@ -102,26 +93,26 @@ class OnAudiosFromQuery { for playlist in cursor { let iPlaylist = playlist as! MPMediaPlaylist let iWhere = self.args["where"] as Any - //Using this check we can define if [where] is the [Playlist] name or id + // Using this check we can define if [where] is the [Playlist] name or id if iWhere is String { - //Check if playlist name is equal to defined name + // Check if playlist name is equal to defined name if iPlaylist.name == iWhere as? String { - //For each song, format and add to the list + // For each song, format and add to the list for song in playlist.items { // If the song file don't has a assetURL, is a Cloud item. - if !song.isCloudItem && song.assetURL != nil { + if !song.isCloudItem, song.assetURL != nil { let songData = loadSongItem(song: song) listOfSongs.append(songData) } } } } else { - //Check if playlist id is equal to defined id + // Check if playlist id is equal to defined id if iPlaylist.persistentID == iWhere as! Int { - //For each song, format and add to the list + // For each song, format and add to the list for song in playlist.items { // If the song file don't has a assetURL, is a Cloud item. - if !song.isCloudItem && song.assetURL != nil { + if !song.isCloudItem, song.assetURL != nil { let songData = loadSongItem(song: song) listOfSongs.append(songData) } diff --git a/on_audio_query/ios/Classes/query/OnAudioQuery.swift b/on_audio_query/ios/Classes/queries/AudioQuery.swift similarity index 51% rename from on_audio_query/ios/Classes/query/OnAudioQuery.swift rename to on_audio_query/ios/Classes/queries/AudioQuery.swift index ec091825..c3c9453f 100644 --- a/on_audio_query/ios/Classes/query/OnAudioQuery.swift +++ b/on_audio_query/ios/Classes/queries/AudioQuery.swift @@ -1,43 +1,37 @@ import Flutter import MediaPlayer -class OnAudioQuery { +class AudioQuery { var args: [String: Any] var result: FlutterResult - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() } func querySongs() { - // The sortType. let sortType = args["sortType"] as? Int ?? 0 - // Choose the type(To match android side, let's call "cursor"). let cursor = MPMediaQuery.songs() + // Using native sort from [IOS] you can only use the [Title], [Album] and // [Artist]. The others will be sorted "manually" using [formatSongList] before // send to Dart. cursor.groupingType = checkSongSortType(sortType: sortType) - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( value: false, forProperty: MPMediaItemPropertyIsCloudItem ) cursor.addFilterPredicate(cloudFilter) - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Query everything in background for a better performance. - loadSongs(cursor: cursor) - } else { - // There's no permission so, return empty to avoid crashes. - result([]) - } + Log.type.debug("Query config: ") + Log.type.debug("\tsortType: \(sortType)") + + // Query everything in background for a better performance. + loadSongs(cursor: cursor) } private func loadSongs(cursor: MPMediaQuery!) { @@ -46,19 +40,17 @@ class OnAudioQuery { // For each item(song) inside this "cursor", take one and "format" // into a [Map], all keys are based on [Android] - // platforms so, if you change some key, will have to change the [Android] too. + // platforms. for song in cursor.items! { - // If the song file don't has a assetURL, is a Cloud item. - if !song.isCloudItem && song.assetURL != nil { + // Ignore cloud items. + if !song.isCloudItem, song.assetURL != nil { let songData = loadSongItem(song: song) listOfSongs.append(songData) } } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). DispatchQueue.main.async { - // Here we'll check the "custom" sort and define a order to the list. + // Custom sort/order. let finalList = formatSongList(args: self.args, allSongs: listOfSongs) self.result(finalList) } diff --git a/on_audio_query/ios/Classes/query/OnGenresQuery.swift b/on_audio_query/ios/Classes/queries/GenreQuery.swift similarity index 55% rename from on_audio_query/ios/Classes/query/OnGenresQuery.swift rename to on_audio_query/ios/Classes/queries/GenreQuery.swift index 1b92d9ae..10f58567 100644 --- a/on_audio_query/ios/Classes/query/OnGenresQuery.swift +++ b/on_audio_query/ios/Classes/queries/GenreQuery.swift @@ -1,40 +1,31 @@ import Flutter import MediaPlayer -class OnGenresQuery { +class GenreQuery { var args: [String: Any] var result: FlutterResult - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() } func queryGenres() { - // Choose the type(To match android side, let's call "cursor"). let cursor = MPMediaQuery.genres() // We don't need to define a sortType here. [IOS] only support // the [Artist]. The others will be sorted "manually" using // [formatSongList] before send to Dart. - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( value: false, forProperty: MPMediaItemPropertyIsCloudItem ) cursor.addFilterPredicate(cloudFilter) - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Query everything in background for a better performance. - loadGenres(cursor: cursor.collections) - } else { - // There's no permission so, return empty to avoid crashes. - result([]) - } + // Query everything in background for a better performance. + loadGenres(cursor: cursor.collections) } private func loadGenres(cursor: [MPMediaItemCollection]!) { @@ -43,9 +34,9 @@ class OnGenresQuery { // For each item(genre) inside this "cursor", take one and "format" // into a [Map], all keys are based on [Android] - // platforms so, if you change some key, will have to change the [Android] too. + // platforms. for genre in cursor { - if !genre.items[0].isCloudItem && genre.items[0].assetURL != nil { + if !genre.items[0].isCloudItem, genre.items[0].assetURL != nil { var genreData = loadGenreItem(genre: genre) // Count and add the number of songs for every genre. @@ -56,10 +47,8 @@ class OnGenresQuery { } } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). DispatchQueue.main.async { - // Here we'll check the "custom" sort and define a order to the list. + // Custom sort/order. let finalList = formatGenreList(args: self.args, allGenres: listOfGenres) self.result(finalList) } diff --git a/on_audio_query/ios/Classes/query/OnPlaylistsQuery.swift b/on_audio_query/ios/Classes/queries/PlaylistQuery.swift similarity index 53% rename from on_audio_query/ios/Classes/query/OnPlaylistsQuery.swift rename to on_audio_query/ios/Classes/queries/PlaylistQuery.swift index 90614dd6..baa1025b 100644 --- a/on_audio_query/ios/Classes/query/OnPlaylistsQuery.swift +++ b/on_audio_query/ios/Classes/queries/PlaylistQuery.swift @@ -1,38 +1,29 @@ import Flutter import MediaPlayer -class OnPlaylistsQuery { +class PlaylistQuery { var args: [String: Any] var result: FlutterResult - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() } func queryPlaylists() { - // Choose the type(To match android side, let's call "cursor"). let cursor = MPMediaQuery.playlists() // TODO: Add sort type to [queryPlaylists]. - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( + // Filter to avoid audios/songs from cloud library. + let cloudFilter = MPMediaPropertyPredicate( value: false, forProperty: MPMediaItemPropertyIsCloudItem ) cursor.addFilterPredicate(cloudFilter) - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Query everything in background for a better performance. - loadPlaylists(cursor: cursor.collections) - } else { - // There's no permission so, return empty to avoid crashes. - result([]) - } + // Query everything in background for a better performance. + loadPlaylists(cursor: cursor.collections) } private func loadPlaylists(cursor: [MPMediaItemCollection]!) { @@ -41,10 +32,10 @@ class OnPlaylistsQuery { // For each item(playlist) inside this "cursor", take one and "format" // into a [Map], all keys are based on [Android] - // platforms so, if you change some key, will have to change the [Android] too. + // platforms. for playlist in cursor { - // If the first song file don't has a assetURL, is a Cloud item. - if !playlist.items[0].isCloudItem && playlist.items[0].assetURL != nil { + // Ignore cloud items. + if !playlist.items[0].isCloudItem, playlist.items[0].assetURL != nil { var playlistData = loadPlaylistItem(playlist: playlist) // Count and add the number of songs for every genre. @@ -55,8 +46,6 @@ class OnPlaylistsQuery { } } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). DispatchQueue.main.async { // TODO: Add sort type to [queryPlaylists]. self.result(listOfPlaylists) diff --git a/on_audio_query/ios/Classes/query/OnWithFiltersQuery.swift b/on_audio_query/ios/Classes/queries/WithFiltersQuery.swift similarity index 62% rename from on_audio_query/ios/Classes/query/OnWithFiltersQuery.swift rename to on_audio_query/ios/Classes/queries/WithFiltersQuery.swift index 7e3c7cf8..ad0ab30c 100644 --- a/on_audio_query/ios/Classes/query/OnWithFiltersQuery.swift +++ b/on_audio_query/ios/Classes/queries/WithFiltersQuery.swift @@ -1,28 +1,29 @@ import Flutter import MediaPlayer -class OnWithFiltersQuery { +class WithFiltersQuery { var args: [String: Any] var result: FlutterResult - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result + init() { + self.args = try! PluginProvider.call().arguments as! [String: Any] + self.result = try! PluginProvider.result() } func queryWithFilters() { // None of this arguments can be null. // The [type] will be used to define where item will be queried. let withType = args["withType"] as! Int + // The [arg] will be used to define the "search". let arg = args["args"] as! Int + // The [argVal] is the "name" to the "search" let argVal = args["argsVal"] as! String // (To match android side, let's call "cursor"). - var cursor: MPMediaQuery? = nil - var filter: MPMediaPropertyPredicate? = nil + var cursor: MPMediaQuery? + var filter: MPMediaPropertyPredicate? // Use the [type] to define the query. switch withType { @@ -46,38 +47,37 @@ class OnWithFiltersQuery { break } - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Choose between query [Playlist] or others. - if filter != nil && withType != 2 { - // Add the filter. - cursor?.addFilterPredicate(filter!) - - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor?.addFilterPredicate(cloudFilter) - - // Query everything in background for a better performance. - loadItemsWithFilter(cursor: cursor!, type: withType) - } else { + Log.type.debug("Query config: ") + Log.type.debug("\twithType: \(withType)") + Log.type.debug("\targ: \(arg)") + Log.type.debug("\targVal: \(argVal)") + Log.type.debug("\tcursor: \(String(describing: cursor))") + Log.type.debug("\tfilter: \(String(describing: filter))") + + // Choose between query [Playlist] or others. + if filter != nil, withType != 2 { + // Add the filter. + cursor?.addFilterPredicate(filter!) - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor?.addFilterPredicate(cloudFilter) + // Ignore cloud items. + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor?.addFilterPredicate(cloudFilter) - // Query everything in background for a better performance. - loadPlaylistsWithFilter(cursor: cursor!.collections, argVal: argVal) - } + // Query everything in background for a better performance. + loadItemsWithFilter(cursor: cursor!, type: withType) } else { - // There's no permission so, return empty to avoid crashes. - result([]) + // Ignore cloud items. + let cloudFilter = MPMediaPropertyPredicate( + value: false, + forProperty: MPMediaItemPropertyIsCloudItem + ) + cursor?.addFilterPredicate(cloudFilter) + + // Query everything in background for a better performance. + loadPlaylistsWithFilter(cursor: cursor!.collections, argVal: argVal) } } @@ -85,15 +85,15 @@ class OnWithFiltersQuery { DispatchQueue.global(qos: .userInitiated).async { var listOfItems: [[String: Any?]] = Array() - // [0]: Song - We use [MPMediaItem]. - // [1, 3 and 4]: Album, Artist and Genre - We use [MPMediaItemCollection]. + // 0 -> Song -> We use [MPMediaItem]. + // 1, 3 or 4 -> Album, Artist or Genre - We use [MPMediaItemCollection]. if type == 0 { // For each item(song) inside this "cursor", take one and "format" // into a [Map], all keys are based on [Android] // platforms so, if you change some key, will have to change the [Android] too. for song in cursor.items! { // If the song file don't has a assetURL, is a Cloud item. - if !song.isCloudItem && song.assetURL != nil { + if !song.isCloudItem, song.assetURL != nil { let songData = loadSongItem(song: song) listOfItems.append(songData) } @@ -106,8 +106,8 @@ class OnWithFiltersQuery { // have to change the [Android] too. for item in cursor.collections! { var itemData: [String: Any?] = [:] - // If the first song file don't has a assetURL, is a Cloud item. - if !item.items[0].isCloudItem && item.items[0].assetURL != nil { + // Ignore cloud items. + if !item.items[0].isCloudItem, item.items[0].assetURL != nil { switch type { case 1: itemData = loadAlbumItem(album: item) @@ -123,10 +123,7 @@ class OnWithFiltersQuery { } } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). DispatchQueue.main.async { - // Back to dart. self.result(listOfItems) } } @@ -145,17 +142,13 @@ class OnWithFiltersQuery { let iPlaylist = playlist as! MPMediaPlaylist // Check if some playlist contains the defined argument. - // If the first song file don't has a assetURL, is a Cloud item. - if iPlaylist.name!.contains(argVal) && !iPlaylist.items[0].isCloudItem { + if iPlaylist.name!.contains(argVal), !iPlaylist.items[0].isCloudItem { playlistData = loadPlaylistItem(playlist: playlist) } listOfPlaylist.append(playlistData) } - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). DispatchQueue.main.async { - // Back to dart. self.result(listOfPlaylist) } } diff --git a/on_audio_query/ios/Classes/query/helper/OnAudioHelper.swift b/on_audio_query/ios/Classes/queries/helper/OnAudioHelper.swift similarity index 80% rename from on_audio_query/ios/Classes/query/helper/OnAudioHelper.swift rename to on_audio_query/ios/Classes/queries/helper/OnAudioHelper.swift index 2311d7d6..5b85dcfe 100644 --- a/on_audio_query/ios/Classes/query/helper/OnAudioHelper.swift +++ b/on_audio_query/ios/Classes/queries/helper/OnAudioHelper.swift @@ -25,7 +25,7 @@ public func loadSongItem(song: MPMediaItem) -> [String: Any?] { "duration": Int(song.playbackDuration * 1000), "title": song.title, "track": song.albumTrackNumber, - "file_extension": fileExt, + "file_extension": fileExt ] return songData } @@ -35,29 +35,29 @@ public func formatSongList(args: [String: Any], allSongs: [[String: Any?]]) -> [ let order = args["orderType"] as? Int let sortType = args["sortType"] as? Int let ignoreCase = args["ignoreCase"] as! Bool - + // switch sortType { case 3: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["duration"] as! Double) > (val2["duration"] as! Double) } case 4: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["date_added"] as! Int) > (val2["date_added"] as! Int) } case 5: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["_size"] as! Int) > (val2["_size"] as! Int) } case 6: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in ((val1["_display_name"] as! String).isCase(ignoreCase: ignoreCase)) > ((val2["_display_name"] as! String).isCase(ignoreCase: ignoreCase)) } default: break } - + // if order == 1 { tempList.reverse() @@ -65,7 +65,7 @@ public func formatSongList(args: [String: Any], allSongs: [[String: Any?]]) -> [ return tempList } -//Albums +// Albums func loadAlbumItem(album: MPMediaItemCollection) -> [String: Any?] { let albumData: [String: Any?] = [ @@ -83,13 +83,13 @@ public func formatAlbumList(args: [String: Any], allAlbums: [[String: Any?]]) -> var tempList = allAlbums let order = args["orderType"] as? Int let sortType = args["sortType"] as? Int - + if sortType == 3 { - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["numsongs"] as! Int) > (val2["numsongs"] as! Int) } } - + // if order == 1 { tempList.reverse() @@ -97,27 +97,25 @@ public func formatAlbumList(args: [String: Any], allAlbums: [[String: Any?]]) -> return tempList } - -//Artists +// Artists func loadArtistItem(artist: MPMediaItemCollection) -> [String: Any?] { - //Get all albums from artist + // Get all albums from artist let albumsCursor = MPMediaQuery.albums() - albumsCursor.addFilterPredicate(MPMediaPropertyPredicate.init(value: artist.items[0].albumArtist, forProperty: MPMediaItemPropertyAlbumArtist)) + albumsCursor.addFilterPredicate(MPMediaPropertyPredicate(value: artist.items[0].albumArtist, forProperty: MPMediaItemPropertyAlbumArtist)) var finalCount: [String] = Array() - + let albums = albumsCursor.collections - - //Normally when song don't have a album, will be "nil" or "unknown", - //Here we'll "filter" the albums, removing this "non-albums". - //So, if multiples songs don't has a defined album, will be count only 1. + + // Normally when song don't have a album, will be null or unknown, + // We'll "filter" the albums, removing this "non-albums". for album in albums! { let itemAlbum = album.items[0].albumTitle - if itemAlbum != nil && !finalCount.contains(itemAlbum!) { + if itemAlbum != nil, !finalCount.contains(itemAlbum!) { finalCount.append(itemAlbum!) } } - + // let artistData: [String: Any?] = [ "_id": artist.items[0].artistPersistentID, @@ -132,20 +130,20 @@ public func formatArtistList(args: [String: Any], allArtists: [[String: Any?]]) var tempList = allArtists let order = args["orderType"] as? Int let sortType = args["sortType"] as? Int - + switch sortType { case 3: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["number_of_tracks"] as! Int) > (val2["number_of_tracks"] as! Int) } case 4: - tempList.sort { (val1, val2) -> Bool in + tempList.sort { val1, val2 -> Bool in (val1["number_of_albums"] as! Int) > (val2["number_of_albums"] as! Int) } default: break } - + // if order == 1 { tempList.reverse() @@ -153,7 +151,7 @@ public func formatArtistList(args: [String: Any], allArtists: [[String: Any?]]) return tempList } -//Genres +// Genres func loadGenreItem(genre: MPMediaItemCollection) -> [String: Any?] { // @@ -168,7 +166,7 @@ func loadGenreItem(genre: MPMediaItemCollection) -> [String: Any?] { public func formatGenreList(args: [String: Any], allGenres: [[String: Any?]]) -> [[String: Any?]] { var tempList = allGenres let order = args["orderType"] as? Int - + // if order == 1 { tempList.reverse() @@ -179,29 +177,29 @@ public func formatGenreList(args: [String: Any], allGenres: [[String: Any?]]) -> public func getMediaCount(type: Int, id: UInt64) -> Int { var cursor: MPMediaQuery? = nil var filter: MPMediaPropertyPredicate? = nil - - if (type == 0) { - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaItemPropertyGenrePersistentID) + + if type == 0 { + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyGenrePersistentID) cursor = MPMediaQuery.genres() } else { - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) + filter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) cursor = MPMediaQuery.playlists() } - - if (cursor != nil && filter != nil) { + + if cursor != nil && filter != nil { cursor?.addFilterPredicate(filter!) - - if (cursor!.collections?.count != nil) { + + if cursor!.collections?.count != nil { return cursor!.collections!.count } } - - return -1; + + return -1 } func loadPlaylistItem(playlist: MPMediaItemCollection) -> [String: Any?] { - //Get the artwork from the first song inside the playlist - var artwork: Data? = nil + // Get the artwork from the first song inside the playlist + var artwork: Data? if playlist.items.count >= 1 { artwork = playlist.items[0].artwork?.image(at: CGSize(width: 150, height: 150))?.jpegData(compressionQuality: 1) } @@ -222,7 +220,7 @@ func loadPlaylistItem(playlist: MPMediaItemCollection) -> [String: Any?] { extension String { func isCase(ignoreCase: Bool) -> String { - if (ignoreCase) { + if ignoreCase { return self } else { return self.lowercased() diff --git a/on_audio_query/ios/Classes/query/OnAlbumsQuery.swift b/on_audio_query/ios/Classes/query/OnAlbumsQuery.swift deleted file mode 100644 index 3065419a..00000000 --- a/on_audio_query/ios/Classes/query/OnAlbumsQuery.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Flutter -import MediaPlayer - -class OnAlbumsQuery { - var args: [String: Any] - var result: FlutterResult - - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result - } - - func queryAlbums() { - // The sortType, this method will never be will. - let sortType = args["sortType"] as? Int ?? 0 - - // Choose the type(To match android side, let's call "cursor"). - let cursor = MPMediaQuery.albums() - // Using native sort from [IOS] you can only use the [Album] and [Artist]. - // The others will be sorted "manually" using [formatAlbumList] before - // send to Dart. - cursor.groupingType = checkAlbumSortType(sortType: sortType) - - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor.addFilterPredicate(cloudFilter) - - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Query everything in background for a better performance. - loadAlbums(cursor: cursor.collections) - } else { - // There's no permission so, return empty to avoid crashes. - result([]) - } - } - - private func loadAlbums(cursor: [MPMediaItemCollection]!) { - DispatchQueue.global(qos: .userInitiated).async { - var listOfAlbums: [[String: Any?]] = Array() - - // For each item(album) inside this "cursor", take one and "format" - // into a [Map], all keys are based on [Android] - // platforms so, if you change some key, will have to change the [Android] too. - for album in cursor { - if !album.items[0].isCloudItem && album.items[0].assetURL != nil { - let albumData = loadAlbumItem(album: album) - listOfAlbums.append(albumData) - } - } - - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). - DispatchQueue.main.async { - // Here we'll check the "custom" sort and define a order to the list. - let finalList = formatAlbumList(args: self.args, allAlbums: listOfAlbums) - self.result(finalList) - } - } - } -} diff --git a/on_audio_query/ios/Classes/query/OnArtistQuery.swift b/on_audio_query/ios/Classes/query/OnArtistQuery.swift deleted file mode 100644 index 05c3bf64..00000000 --- a/on_audio_query/ios/Classes/query/OnArtistQuery.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Flutter -import MediaPlayer - -class OnArtistsQuery { - var args: [String: Any] - var result: FlutterResult - - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result - } - - func queryArtists() { - // Choose the type(To match android side, let's call "cursor"). - let cursor = MPMediaQuery.artists() - - // We don't need to define a sortType here. [IOS] only support - // the [Artist]. The others will be sorted "manually" using - // [formatSongList] before send to Dart. - - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor.addFilterPredicate(cloudFilter) - - // We cannot "query" without permission so, just return a empty list. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if hasPermission { - // Query everything in background for a better performance. - loadArtists(cursor: cursor.collections) - } else { - // There's no permission so, return empty to avoid crashes. - result([]) - } - } - - private func loadArtists(cursor: [MPMediaItemCollection]!) { - DispatchQueue.global(qos: .userInitiated).async { - var listOfArtists: [[String: Any?]] = Array() - - // For each item(artist) inside this "cursor", take one and "format" - // into a [Map], all keys are based on [Android] - // platforms so, if you change some key, will have to change the [Android] too. - for artist in cursor { - // If the first song file don't has a assetURL, is a Cloud item. - if !artist.items[0].isCloudItem && artist.items[0].assetURL != nil { - let artistData = loadArtistItem(artist: artist) - listOfArtists.append(artistData) - } - } - - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). - DispatchQueue.main.async { - // Here we'll check the "custom" sort and define a order to the list. - let finalList = formatArtistList(args: self.args, allArtists: listOfArtists) - self.result(finalList) - } - } - } -} diff --git a/on_audio_query/ios/Classes/query/OnArtworkQuery.swift b/on_audio_query/ios/Classes/query/OnArtworkQuery.swift deleted file mode 100644 index 0a4324e3..00000000 --- a/on_audio_query/ios/Classes/query/OnArtworkQuery.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Flutter -import MediaPlayer - -class OnArtworkQuery { - var args: [String: Any] - var result: FlutterResult - - init(call: FlutterMethodCall, result: @escaping FlutterResult) { - // To make life easy, add all arguments inside a map. - self.args = call.arguments as! [String: Any] - self.result = result - } - - // [IOS] has a different artwork system and you can "query" using normal "querySongs, .." - // [Android] can't "query" artwork at the same time as "querySongs", so we need to "query" - // using a different method(queryArtwork). - // - // To match both [IOS] and [Android], [queryArtwork] is the only way to get artwork. - // - // Not the best solution but, at least here we can select differents formats and size. - func queryArtwork() { - // None of this arguments can be null. - // The id of the [Song] or [Album]. - let id = args["id"] as! Int - // The size of the image. - let size = args["size"] as! Int - // The size of the image. - var quality = args["quality"] as! Int - if (quality > 100) { - quality = 100 - } - // The format [JPEG] or [PNG]. - let format = args["format"] as! Int - // The uri [0]: Song and [1]: Album. - let uri = args["type"] as! Int - - // (To match android side, let's call "cursor"). - var cursor: MPMediaQuery? - var filter: MPMediaPropertyPredicate? - // If [uri] is 0: artwork from [Song] - // If [uri] is 1: artwork from [Album] - // If [uri] is 2: artwork from [Playlist] - // If [uri] is 3: artwork from [Artist] - switch uri { - case 0: - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaItemPropertyPersistentID) - cursor = MPMediaQuery.songs() - case 1: - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaItemPropertyAlbumPersistentID) - cursor = MPMediaQuery.albums() - case 2: - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaPlaylistPropertyPersistentID) - cursor = MPMediaQuery.playlists() - case 3: - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaItemPropertyArtistPersistentID) - cursor = MPMediaQuery.artists() - case 4: - filter = MPMediaPropertyPredicate.init(value: id, forProperty: MPMediaItemPropertyGenrePersistentID) - cursor = MPMediaQuery.genres() - default: - filter = nil - cursor = nil - } - - // If [cursor] is "nil" or has no permission, just return to dart. - let hasPermission = SwiftOnAudioQueryPlugin().checkPermission() - if cursor != nil && filter != nil && hasPermission { - cursor?.addFilterPredicate(filter!) - - // This filter will avoid audios/songs outside phone library(cloud). - let cloudFilter = MPMediaPropertyPredicate.init( - value: false, - forProperty: MPMediaItemPropertyIsCloudItem - ) - cursor?.addFilterPredicate(cloudFilter) - - // Query everything in background for a better performance. - loadArtwork(cursor: cursor, size: size, format: format, uri: uri, quality: quality) - } else { - // There's no permission so, return null to avoid crashes. - result(nil) - } - } - - private func loadArtwork(cursor: MPMediaQuery!, size: Int, format: Int, uri: Int, quality: Int) { - DispatchQueue.global(qos: .userInitiated).async { - var tempArtwork: Data? - var tempItem: MPMediaItem? - let fixedQuality = CGFloat(Double(quality) / 100.0) - - // If [uri] is 0: artwork is from [Song] - // If [uri] is 1, 2 or 3: artwork is from [Album], [Playlist] or [Artist] - if uri == 0 { - // Since all id are unique, we can safely call the first item. - tempItem = cursor!.items?.first - } else { - // Since all id are unique, we can safely call the first item. - tempItem = cursor!.collections?.first?.items[0] - } - - // If [format] is 0: will be [JPEG] - // If [format] is 1: will be [PNG] - if format == 0 { - tempArtwork = tempItem?.artwork?.image(at: CGSize(width: size, height: size))?.jpegData(compressionQuality: fixedQuality) - } else { - // [PNG] format will return a high image quality. - tempArtwork = tempItem?.artwork?.image(at: CGSize(width: size, height: size))?.pngData() - } - - // After finish the "query", go back to the "main" thread(You can only call flutter - // inside the main thread). - DispatchQueue.main.async { - // We don't need a "empty" image so, return null to avoid problems. - if tempArtwork != nil && tempArtwork!.isEmpty { - tempArtwork = nil - } - self.result(tempArtwork) - } - } - } -} diff --git a/on_audio_query/ios/Classes/types/AudiosFromType.swift b/on_audio_query/ios/Classes/types/AudiosFromType.swift index fb67d005..7167fdcb 100644 --- a/on_audio_query/ios/Classes/types/AudiosFromType.swift +++ b/on_audio_query/ios/Classes/types/AudiosFromType.swift @@ -1,21 +1,21 @@ import MediaPlayer func checkAudiosFrom(type: Int, where: Any) -> MPMediaQuery? { - var filter: MPMediaPropertyPredicate? = nil + var filter: MPMediaPropertyPredicate? let query = MPMediaQuery.songs() switch type { case 0: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyAlbumTitle) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyAlbumTitle) case 1: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyAlbumPersistentID) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyAlbumPersistentID) case 2: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyArtist) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyArtist) case 3: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyArtistPersistentID) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyArtistPersistentID) case 4: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyGenre) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyGenre) case 5: - filter = MPMediaPropertyPredicate.init(value: `where`, forProperty: MPMediaItemPropertyGenrePersistentID) + filter = MPMediaPropertyPredicate(value: `where`, forProperty: MPMediaItemPropertyGenrePersistentID) default: return nil } diff --git a/on_audio_query/ios/Classes/types/WithFiltersType.swift b/on_audio_query/ios/Classes/types/WithFiltersType.swift index a6d360f8..e497bd2c 100644 --- a/on_audio_query/ios/Classes/types/WithFiltersType.swift +++ b/on_audio_query/ios/Classes/types/WithFiltersType.swift @@ -4,14 +4,14 @@ func checkSongsArgs(args: Int, argsVal: String) -> MPMediaPropertyPredicate { var filter: MPMediaPropertyPredicate? = nil switch args { case 0: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyTitle, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyTitle, comparisonType: .contains) case 1: print("[on_audio_warning] - IOS don't support [DISPLAY_NAME] type. Will be used the [TITLE]") - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyTitle, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyTitle, comparisonType: .contains) case 2: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyAlbumTitle, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyAlbumTitle, comparisonType: .contains) case 3: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyArtist, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyArtist, comparisonType: .contains) default: break } @@ -22,22 +22,22 @@ func checkAlbumsArgs(args: Int, argsVal: String) -> MPMediaPropertyPredicate { var filter: MPMediaPropertyPredicate? = nil switch args { case 0: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyAlbumTitle, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyAlbumTitle, comparisonType: .contains) case 1: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyAlbumArtist, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyAlbumArtist, comparisonType: .contains) default: break } return filter! } -//Playlist +// Playlist func checkArtistsArgs(args: Int, argsVal: String) -> MPMediaPropertyPredicate { var filter: MPMediaPropertyPredicate? = nil switch args { case 0: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyArtist, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyArtist, comparisonType: .contains) default: break } @@ -48,7 +48,7 @@ func checkGenresArgs(args: Int, argsVal: String) -> MPMediaPropertyPredicate { var filter: MPMediaPropertyPredicate? = nil switch args { case 0: - filter = MPMediaPropertyPredicate.init(value: argsVal, forProperty: MPMediaItemPropertyGenre, comparisonType: .contains) + filter = MPMediaPropertyPredicate(value: argsVal, forProperty: MPMediaItemPropertyGenre, comparisonType: .contains) default: break } diff --git a/on_audio_query/ios/Classes/utils/Log.swift b/on_audio_query/ios/Classes/utils/Log.swift new file mode 100644 index 00000000..eabf0554 --- /dev/null +++ b/on_audio_query/ios/Classes/utils/Log.swift @@ -0,0 +1,40 @@ +import SwiftyBeaver + +class Log { + private init() {} + + private static let format = "$Dyyyy-MM-dd $DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M" + + private static let console = ConsoleDestination() + + static let type = SwiftyBeaver.self + + static func setLogLevel(level: SwiftyBeaver.Level) { + type.addDestination(console) + console.format = format + console.minLevel = level + } + + static func setLogLevel(dartLevel: Int) { + type.addDestination(console) + console.format = format + console.minLevel = convertLogLevel(dartLevel) + } + + private static func convertLogLevel(_ dartLevel: Int) -> SwiftyBeaver.Level { + switch dartLevel { + case 2: + return SwiftyBeaver.Level.verbose + case 3: + return SwiftyBeaver.Level.debug + case 4: + return SwiftyBeaver.Level.info + case 5: + return SwiftyBeaver.Level.warning + case 6: + return SwiftyBeaver.Level.error + default: + return SwiftyBeaver.Level.warning + } + } +} diff --git a/on_audio_query/ios/Classes/utils/OnDeviceInfo.swift b/on_audio_query/ios/Classes/utils/OnDeviceInfo.swift deleted file mode 100644 index b0466a4d..00000000 --- a/on_audio_query/ios/Classes/utils/OnDeviceInfo.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Flutter - -// TODO: Add more specific return to [Model]. -// This method will get some basic information about platform. -public func queryDeviceInfo(result: FlutterResult) { - let device = UIDevice.current - let deviceData: [String: Any] = [ - "device_model": device.model, - "device_sys_type": device.systemName, - "device_sys_version": device.systemVersion, - ] - result(deviceData) -} - diff --git a/on_audio_query/ios/on_audio_query.podspec b/on_audio_query/ios/on_audio_query.podspec index 7b764af7..34e98111 100644 --- a/on_audio_query/ios/on_audio_query.podspec +++ b/on_audio_query/ios/on_audio_query.podspec @@ -4,10 +4,10 @@ # Pod::Spec.new do |s| s.name = 'on_audio_query' - s.version = '2.5.0' - s.summary = 'Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage.' + s.version = '0.0.1' + s.summary = 'Audio query plugin for flutter.' s.description = <<-DESC -A new flutter plugin project. + Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage. DESC s.homepage = 'https://github.com/LucasPJS/on_audio_query' s.license = { :file => '../LICENSE' } @@ -15,6 +15,7 @@ A new flutter plugin project. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' + s.dependency 'SwiftyBeaver' s.platform = :ios, '10.0' # Flutter.framework does not contain a i386 slice. diff --git a/on_audio_query/lib/details/on_audio_query_controller.dart b/on_audio_query/lib/details/on_audio_query_controller.dart index b52c31c5..fbef6742 100644 --- a/on_audio_query/lib/details/on_audio_query_controller.dart +++ b/on_audio_query/lib/details/on_audio_query_controller.dart @@ -36,6 +36,40 @@ class OnAudioQuery { } } + /// Simplified version of [permissionsStatus] and [permissionsRequest]. + /// + /// Will check and request, if necessary, all required permissions. + /// + /// **OBS: Will always return true on web platform.** + Future checkAndRequest({bool retryRequest = false}) async { + if (kIsWeb) return true; + + bool hasPermission = await platform.permissionsStatus(); + if (!hasPermission) { + hasPermission = await platform.permissionsRequest( + retryRequest: retryRequest, + ); + } + + return hasPermission; + } + + /// Used to set the logging behavior. + /// + /// Parameters: + /// + /// * [logType] is used to define the logging level. [LogType]. + /// * [detailedLog] is used to define if detailed log will be shown + /// (Disable by default to avoid spam). + /// + /// Important: + /// + /// * If [logType] is null, will be set to [WARN]. + /// * If [detailedLog] is null, will be set to [false]. + Future setLogConfig(LogConfig? logConfig) async { + return platform.setLogConfig(logConfig); + } + /// Used to return Songs Info based in [SongModel]. /// /// Parameters: @@ -535,7 +569,8 @@ class OnAudioQuery { /// Important: /// /// * This method will always return a bool. - /// * If return true `[READ]` and `[WRITE]` permissions is Granted, else `[READ]` and `[WRITE]` is Denied. + /// * If return true `[READ]` and `[WRITE]` permissions is Granted, + /// else `[READ]` and `[WRITE]` is Denied. /// /// Platforms: /// @@ -553,7 +588,8 @@ class OnAudioQuery { /// Important: /// /// * This method will always return a bool. - /// * If return true `[READ]` and `[WRITE]` permissions is Granted, else `[READ]` and `[WRITE]` is Denied. + /// * If return true `[READ]` and `[WRITE]` permissions is Granted, + /// else `[READ]` and `[WRITE]` is Denied. /// /// Platforms: /// @@ -562,8 +598,8 @@ class OnAudioQuery { /// | `✔️` | `✔️` | `❌` |
/// /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLATFORMS.md) - Future permissionsRequest() async { - return platform.permissionsRequest(); + Future permissionsRequest({bool retryRequest = false}) async { + return platform.permissionsRequest(retryRequest: retryRequest); } // Device Information diff --git a/on_audio_query/lib/on_audio_query.dart b/on_audio_query/lib/on_audio_query.dart index a8898968..1e23f22a 100644 --- a/on_audio_query/lib/on_audio_query.dart +++ b/on_audio_query/lib/on_audio_query.dart @@ -16,9 +16,9 @@ library on_audio_query; // import 'dart:async'; -import 'dart:typed_data'; //Dart/Flutter +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; //Platform Interface diff --git a/on_audio_query/lib/widget/query_artwork_widget.dart b/on_audio_query/lib/widget/query_artwork_widget.dart index ffe9593f..d199d110 100644 --- a/on_audio_query/lib/widget/query_artwork_widget.dart +++ b/on_audio_query/lib/widget/query_artwork_widget.dart @@ -18,6 +18,11 @@ part of on_audio_query; /// /// A simple example on how you can use the [queryArtwork]. /// +/// Important: +/// +/// * If [controller] is null, will be create a new instance. +/// * Log set with [setLogConfig] will only work if [controller] is not null. +/// /// See more: [QueryArtworkWidget](https://pub.dev/documentation/on_audio_query/latest/on_audio_query/QueryArtworkWidget-class.html) class QueryArtworkWidget extends StatelessWidget { /// Used to find and get image. @@ -25,6 +30,14 @@ class QueryArtworkWidget extends StatelessWidget { /// All Audio/Song has a unique [id]. final int id; + /// Used to call the platform specific method. + /// + /// Important: + /// + /// * If [controller] is null, will be create a new instance. + /// * Log set with [setLogConfig] will only work if [controller] is not null. + final OnAudioQuery? controller; + /// Used to define artwork [type]. /// /// Opts: [AUDIO] and [ALBUM]. @@ -36,23 +49,23 @@ class QueryArtworkWidget extends StatelessWidget { /// /// Important: /// - /// * If [format] is null, will be set to [JPEG]. - final ArtworkFormat? format; + /// * If [format] is not defined, will be set to [JPEG]. + final ArtworkFormat format; /// Used to define artwork [size]. /// /// Important: /// - /// * If [size] is null, will be set to [200]. + /// * If [size] is not defined, will be set to [200]. /// * This value have a directly influence to image quality. - final int? size; + final int size; /// Used to define artwork [quality]. /// /// Important: /// - /// * If [quality] is null, will be set to [100]. - final int? quality; + /// * If [quality] is not defined, will be set to [100]. + final int quality; /// Used to define the artwork [border radius]. /// @@ -65,51 +78,51 @@ class QueryArtworkWidget extends StatelessWidget { /// /// Important: /// - /// * If [artworkQuality] is null, will be set to [low]. - /// * This value [don't] have a directly influence to image quality. - final FilterQuality? artworkQuality; + /// * If [artworkQuality] is not defined, will be set to [low]. + /// * This value doesn't have a directly influence to image quality. + final FilterQuality artworkQuality; /// Used to define artwork [width]. /// /// Important: /// - /// * If [artworkWidth] is null, will be set to [50]. - final double? artworkWidth; + /// * If [artworkWidth] is not defined, will be set to [50]. + final double artworkWidth; /// Used to define artwork [height]. /// /// Important: /// - /// * If [artworkHeight] is null, will be set to [50]. - final double? artworkHeight; + /// * If [artworkHeight] is not defined, will be set to [50]. + final double artworkHeight; /// Used to define artwork [fit]. /// /// Important: /// - /// * If [artworkFit] is null, will be set to [cover]. - final BoxFit? artworkFit; + /// * If [artworkFit] is not defined, will be set to [cover]. + final BoxFit artworkFit; /// Used to define artwork [clip]. /// /// Important: /// - /// * If [artworkClipBehavior] is null, will be set to [antiAlias]. - final Clip? artworkClipBehavior; + /// * If [artworkClipBehavior] is not defined, will be set to [antiAlias]. + final Clip artworkClipBehavior; /// Used to define artwork [scale]. /// /// Important: /// - /// * If [artworkScale] is null, will be set to [1.0]. - final double? artworkScale; + /// * If [artworkScale] is not defined, will be set to [1.0]. + final double artworkScale; /// Used to define if artwork should [repeat]. /// /// Important: /// - /// * If [artworkRepeat] is null, will be set to [false]. - final ImageRepeat? artworkRepeat; + /// * If [artworkRepeat] is not defined, will be set to [false]. + final ImageRepeat artworkRepeat; /// Used to define artwork [color]. /// @@ -149,8 +162,8 @@ class QueryArtworkWidget extends StatelessWidget { /// /// Important: /// - /// * If [keepOldArtwork] is null, will be set to [false]. - final bool? keepOldArtwork; + /// * If [keepOldArtwork] is not defined, will be set to [false]. + final bool keepOldArtwork; /// Used to define a Widget when audio/song don't return any artwork. /// @@ -251,56 +264,53 @@ class QueryArtworkWidget extends StatelessWidget { Key? key, required this.id, required this.type, - this.format, - this.size, - this.quality, - this.artworkQuality, + this.quality = 50, + this.controller, + this.format = ArtworkFormat.JPEG, + this.size = 200, + this.artworkQuality = FilterQuality.low, this.artworkBorder, - this.artworkWidth, - this.artworkHeight, - this.artworkFit, - this.artworkClipBehavior, - this.artworkScale, - this.artworkRepeat, + this.artworkWidth = 50, + this.artworkHeight = 50, + this.artworkFit = BoxFit.cover, + this.artworkClipBehavior = Clip.antiAlias, + this.artworkScale = 1.0, + this.artworkRepeat = ImageRepeat.noRepeat, this.artworkColor, this.artworkBlendMode, - this.keepOldArtwork, + this.keepOldArtwork = false, this.nullArtworkWidget, this.errorBuilder, this.frameBuilder, - }) : super(key: key); + }) : assert(quality <= 100), + super(key: key); @override Widget build(BuildContext context) { - if (quality != null && quality! > 100) { - throw Exception( - '[quality] value cannot be greater than [100]', - ); - } return FutureBuilder( - future: OnAudioQuery().queryArtwork( + future: (controller ?? OnAudioQuery()).queryArtwork( id, type, - format: format ?? ArtworkFormat.JPEG, - size: size ?? 200, - quality: quality ?? 100, + format: format, + size: size, + quality: quality, ), builder: (context, item) { if (item.data != null && item.data!.isNotEmpty) { return ClipRRect( borderRadius: artworkBorder ?? BorderRadius.circular(50), - clipBehavior: artworkClipBehavior ?? Clip.antiAlias, + clipBehavior: artworkClipBehavior, child: Image.memory( item.data!, - gaplessPlayback: keepOldArtwork ?? false, - repeat: artworkRepeat ?? ImageRepeat.noRepeat, - scale: artworkScale ?? 1.0, - width: artworkWidth ?? 50, - height: artworkHeight ?? 50, - fit: artworkFit ?? BoxFit.cover, + gaplessPlayback: keepOldArtwork, + repeat: artworkRepeat, + scale: artworkScale, + width: artworkWidth, + height: artworkHeight, + fit: artworkFit, color: artworkColor, colorBlendMode: artworkBlendMode, - filterQuality: artworkQuality ?? FilterQuality.low, + filterQuality: artworkQuality, frameBuilder: frameBuilder, errorBuilder: errorBuilder ?? (context, exception, stackTrace) { diff --git a/on_audio_query/pubspec.yaml b/on_audio_query/pubspec.yaml index 6f845a28..6b44fd9d 100644 --- a/on_audio_query/pubspec.yaml +++ b/on_audio_query/pubspec.yaml @@ -1,7 +1,8 @@ name: on_audio_query description: Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage. -version: 2.6.2 +version: 2.7.0 homepage: https://github.com/LucJosin/on_audio_query/tree/main/on_audio_query +issue_tracker: https://github.com/LucJosin/on_audio_query/issues # pub.dev: https://pub.dev/packages/on_audio_query # ======== # author: Lucas Josino @@ -9,13 +10,15 @@ homepage: https://github.com/LucJosin/on_audio_query/tree/main/on_audio_query # website: https://www.lucasjosino.com/ environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.20.0" dependencies: # on_audio_query - on_audio_query_platform_interface: ^1.4.0 - on_audio_query_web: ^1.3.2+2 + on_audio_query_platform_interface: + path: ../on_audio_query_platform_interface + on_audio_query_web: + path: ../on_audio_query_web # Flutter flutter: diff --git a/on_audio_query_platform_interface/CHANGELOG.md b/on_audio_query_platform_interface/CHANGELOG.md index 3dada8d9..efca5f17 100644 --- a/on_audio_query_platform_interface/CHANGELOG.md +++ b/on_audio_query_platform_interface/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.5.0 - [29.02.2022] +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). + ## 1.4.0 - [02.01.2022] - See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). diff --git a/on_audio_query_platform_interface/lib/details/log/log_config.dart b/on_audio_query_platform_interface/lib/details/log/log_config.dart new file mode 100644 index 00000000..b9d783ec --- /dev/null +++ b/on_audio_query_platform_interface/lib/details/log/log_config.dart @@ -0,0 +1,16 @@ +part of on_audio_query_helper; + +/// Used to configure the logging behavior. +class LogConfig { + /// Used to configure the logging behavior. + LogConfig({ + this.logType = LogType.WARN, + this.detailedLog = false, + }); + + /// Define the logging level. + final LogType logType; + + /// Define if detailed log will be shown (Disable by default to avoid spam). + final bool detailedLog; +} diff --git a/on_audio_query_platform_interface/lib/details/on_audio_query_helper.dart b/on_audio_query_platform_interface/lib/details/on_audio_query_helper.dart index 6bd08e61..da09cb89 100644 --- a/on_audio_query_platform_interface/lib/details/on_audio_query_helper.dart +++ b/on_audio_query_platform_interface/lib/details/on_audio_query_helper.dart @@ -20,7 +20,11 @@ part 'types/sort_types/playlist_sort_type.dart'; part 'types/sort_types/genre_sort_type.dart'; // part 'types/order_type.dart'; +part 'types/log_type.dart'; part 'types/artwork_type.dart'; part 'types/audios_from_type.dart'; part 'types/with_filters_type.dart'; part 'types/uri_type.dart'; + +//Log +part 'log/log_config.dart'; diff --git a/on_audio_query_platform_interface/lib/details/types/log_type.dart b/on_audio_query_platform_interface/lib/details/types/log_type.dart new file mode 100644 index 00000000..72df1bc9 --- /dev/null +++ b/on_audio_query_platform_interface/lib/details/types/log_type.dart @@ -0,0 +1,50 @@ +// ignore_for_file: constant_identifier_names + +part of on_audio_query_helper; + +/// Used to represent the various levels of logs. +enum LogType { + /// Show all logs. + /// + /// Level: 2 + VERBOSE(2), + + /// Show debug. + /// + /// Will also log: + /// * [DEBUG] + /// * [INFO] + /// * [WARN] + /// * [ERROR] + /// + /// Level: 3 + DEBUG(3), + + /// Show some informations. + /// + /// Will also log + /// * [INFO] + /// * [WARN] + /// * [ERROR] + /// + /// Level: 4 + INFO(4), + + /// Show only warnings. + /// + /// Will also log: + /// * [ERROR] + /// + /// Level: 5 + WARN(5), + + /// Show only errors. + /// + /// Level: 6 + ERROR(6); + + const LogType(this.value); + + /// Int value that represent the log. + final int value; +} diff --git a/on_audio_query_platform_interface/lib/method_channel_on_audio_query.dart b/on_audio_query_platform_interface/lib/method_channel_on_audio_query.dart index 4491a8ce..f39b489f 100644 --- a/on_audio_query_platform_interface/lib/method_channel_on_audio_query.dart +++ b/on_audio_query_platform_interface/lib/method_channel_on_audio_query.dart @@ -14,7 +14,6 @@ Copyright: © 2021, Lucas Josino. All rights reserved. */ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -29,6 +28,18 @@ class MethodChannelOnAudioQuery extends OnAudioQueryPlatform { /// The MethodChannel that is being used by this implementation of the plugin. MethodChannel get channel => _channel; + LogConfig _logConfig = LogConfig(); + + @override + Future setLogConfig(LogConfig? logConfig) async { + // Override log configuration + if (logConfig != null) _logConfig = logConfig; + + await _channel.invokeMethod("setLogConfig", { + "level": _logConfig.logType.value, + }); + } + @override Future> querySongs({ SongSortType? sortType, @@ -189,7 +200,8 @@ class MethodChannelOnAudioQuery extends OnAudioQueryPlatform { "id": id, "format": format != null ? format.index : ArtworkFormat.JPEG.index, "size": size ?? 200, - "quality": (quality != null && quality <= 100) ? size : 100, + "quality": (quality != null && quality <= 100) ? quality : 50, + "detailedErrors": _logConfig.detailedLog, }, ); return finalArtworks; @@ -309,9 +321,12 @@ class MethodChannelOnAudioQuery extends OnAudioQueryPlatform { } @override - Future permissionsRequest() async { + Future permissionsRequest({bool retryRequest = false}) async { final bool resultRequest = await _channel.invokeMethod( "permissionsRequest", + { + "retryRequest": retryRequest, + }, ); return resultRequest; } diff --git a/on_audio_query_platform_interface/lib/on_audio_query_platform_interface.dart b/on_audio_query_platform_interface/lib/on_audio_query_platform_interface.dart index a4a04ccb..e90c431a 100644 --- a/on_audio_query_platform_interface/lib/on_audio_query_platform_interface.dart +++ b/on_audio_query_platform_interface/lib/on_audio_query_platform_interface.dart @@ -48,6 +48,22 @@ abstract class OnAudioQueryPlatform extends PlatformInterface { _instance = instance; } + /// Used to set the logging behavior. + /// + /// Parameters: + /// + /// * [logType] is used to define the logging level. [LogType]. + /// * [detailedLog] is used to define if detailed log will be shown + /// (Disable by default to avoid spam). + /// + /// Important: + /// + /// * If [logType] is null, will be set to [WARN]. + /// * If [detailedLog] is null, will be set to [false]. + Future setLogConfig(LogConfig? logConfig) { + throw UnimplementedError('setLogConfig() has not been implemented.'); + } + /// Used to return Songs Info based in [SongModel]. /// /// Parameters: @@ -523,7 +539,7 @@ abstract class OnAudioQueryPlatform extends PlatformInterface { /// | `✔️` | `✔️` | `❌` |
/// /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/PLATFORMS.md) - Future permissionsRequest() { + Future permissionsRequest({bool retryRequest = false}) { throw UnimplementedError('permissionsRequest() has not been implemented.'); } diff --git a/on_audio_query_platform_interface/pubspec.yaml b/on_audio_query_platform_interface/pubspec.yaml index a0b19951..b501ce6a 100644 --- a/on_audio_query_platform_interface/pubspec.yaml +++ b/on_audio_query_platform_interface/pubspec.yaml @@ -3,10 +3,10 @@ description: A common platform interface for the [on_audio_query] plugin. homepage: https://github.com/LucJosin/on_audio_query/tree/master/on_audio_query_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.4.0 +version: 1.5.0 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.0.0" dependencies: diff --git a/on_audio_query_web/CHANGELOG.md b/on_audio_query_web/CHANGELOG.md index bb06435f..10c0be47 100644 --- a/on_audio_query_web/CHANGELOG.md +++ b/on_audio_query_web/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.4.0 - [29.03.2022] +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). + ## 1.3.2+2 - [02.01.2022] - See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). diff --git a/on_audio_query_web/lib/on_audio_query_web.dart b/on_audio_query_web/lib/on_audio_query_web.dart index 44f78be5..0156c255 100644 --- a/on_audio_query_web/lib/on_audio_query_web.dart +++ b/on_audio_query_web/lib/on_audio_query_web.dart @@ -18,7 +18,6 @@ library on_audio_query_web; import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; -import 'dart:typed_data'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:on_audio_query_platform_interface/details/on_audio_query_helper.dart'; diff --git a/on_audio_query_web/pubspec.yaml b/on_audio_query_web/pubspec.yaml index 7d00d689..ccdc4967 100644 --- a/on_audio_query_web/pubspec.yaml +++ b/on_audio_query_web/pubspec.yaml @@ -1,15 +1,16 @@ name: on_audio_query_web description: The web implementation of [on_audio_query]. -version: 1.3.2+2 +version: 1.4.0 homepage: https://github.com/LucJosin/on_audio_query/tree/master/on_audio_query_web environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.20.0" dependencies: # on_audio_query - on_audio_query_platform_interface: ^1.4.0 + on_audio_query_platform_interface: + path: ../on_audio_query_platform_interface # Others path: ^1.8.0