├── .idea
├── .name
├── .gitignore
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── AndroidProjectSystem.xml
├── discord.xml
├── migrations.xml
├── deviceManager.xml
├── misc.xml
├── deploymentTargetSelector.xml
├── gradle.xml
└── runConfigurations.xml
├── app
├── .gitignore
├── src
│ └── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ └── com
│ │ │ └── alexmercerind
│ │ │ └── audire
│ │ │ ├── utils
│ │ │ ├── Constants.kt
│ │ │ └── WaveView.kt
│ │ │ ├── api
│ │ │ └── shazam
│ │ │ │ ├── models
│ │ │ │ ├── Genres.kt
│ │ │ │ ├── ShazamResponse.kt
│ │ │ │ ├── Metadata.kt
│ │ │ │ ├── Signature.kt
│ │ │ │ ├── Section.kt
│ │ │ │ ├── Geolocation.kt
│ │ │ │ ├── Images.kt
│ │ │ │ ├── ShazamRequestBody.kt
│ │ │ │ └── Track.kt
│ │ │ │ └── ShazamAPI.kt
│ │ │ ├── repository
│ │ │ ├── IdentifyRepository.kt
│ │ │ ├── HistoryRepository.kt
│ │ │ ├── ImportExportRepository.kt
│ │ │ ├── SettingsRepository.kt
│ │ │ └── ShazamIdentifyRepository.kt
│ │ │ ├── mappers
│ │ │ ├── ByteArray.kt
│ │ │ ├── HistoryItem.kt
│ │ │ ├── Music.kt
│ │ │ └── ShazamResponse.kt
│ │ │ ├── models
│ │ │ ├── Music.kt
│ │ │ └── HistoryItem.kt
│ │ │ ├── Audire.kt
│ │ │ ├── native
│ │ │ └── ShazamSignature.kt
│ │ │ ├── ui
│ │ │ ├── IdentifyViewModel.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── HistoryViewModel.kt
│ │ │ ├── AboutActivity.kt
│ │ │ ├── adapters
│ │ │ │ └── HistoryItemAdapter.kt
│ │ │ ├── MusicActivity.kt
│ │ │ ├── HistoryFragment.kt
│ │ │ ├── LikedFragment.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ └── IdentifyFragment.kt
│ │ │ ├── db
│ │ │ ├── HistoryItemDao.kt
│ │ │ └── HistoryItemDatabase.kt
│ │ │ ├── services
│ │ │ └── FindMusicTileService.kt
│ │ │ └── audio
│ │ │ ├── AudioIdentifier.kt
│ │ │ └── AudioRecorder.kt
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── xml
│ │ │ ├── file_paths.xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── values
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── colors.xml
│ │ │ ├── themes.xml
│ │ │ └── strings.xml
│ │ ├── drawable
│ │ │ ├── baseline_stop_24.xml
│ │ │ ├── baseline_file_download_24.xml
│ │ │ ├── baseline_file_upload_24.xml
│ │ │ ├── baseline_arrow_back_24.xml
│ │ │ ├── baseline_clear_24.xml
│ │ │ ├── baseline_graphic_eq_24.xml
│ │ │ ├── baseline_info_24.xml
│ │ │ ├── baseline_music_note_24.xml
│ │ │ ├── x.xml
│ │ │ ├── outline_description_24.xml
│ │ │ ├── baseline_calendar_today_24.xml
│ │ │ ├── baseline_label_24.xml
│ │ │ ├── baseline_favorite_24.xml
│ │ │ ├── baseline_mic_24.xml
│ │ │ ├── baseline_album_24.xml
│ │ │ ├── outline_person_24.xml
│ │ │ ├── baseline_search_24.xml
│ │ │ ├── baseline_history_24.xml
│ │ │ ├── outline_lock_24.xml
│ │ │ ├── baseline_favorite_border_24.xml
│ │ │ ├── baseline_share_24.xml
│ │ │ ├── github.xml
│ │ │ ├── baseline_settings_24.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── menu
│ │ │ ├── identify_material_toolbar_menu.xml
│ │ │ ├── bottom_navigation_view_menu.xml
│ │ │ └── history_material_toolbar_menu.xml
│ │ ├── values-night
│ │ │ └── themes.xml
│ │ ├── values-v23
│ │ │ └── themes.xml
│ │ ├── values-night-v23
│ │ │ └── themes.xml
│ │ ├── values-v29
│ │ │ └── themes.xml
│ │ ├── values-night-v29
│ │ │ └── themes.xml
│ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── history_item.xml
│ │ │ ├── fragment_identify.xml
│ │ │ └── fragment_history.xml
│ │ ├── navigation
│ │ │ └── content_nav_graph.xml
│ │ ├── layout-v28
│ │ │ └── history_item.xml
│ │ ├── values-cs
│ │ │ └── strings.xml
│ │ ├── values-it
│ │ │ └── strings.xml
│ │ └── values-fr
│ │ │ └── strings.xml
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
├── schemas
│ └── com.alexmercerind.audire.db.HistoryItemDatabase
│ │ ├── 1.json
│ │ └── 2.json
└── build.gradle.kts
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── short_description.txt
│ ├── video.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 01.jpg
│ │ ├── 02.jpg
│ │ ├── 03.jpg
│ │ ├── 04.jpg
│ │ ├── 05.jpg
│ │ ├── 06.jpg
│ │ ├── 07.jpg
│ │ └── 08.jpg
│ └── full_description.txt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── .github
└── workflows
│ └── android.yml
├── README.md
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Audire
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /src/main/jniLibs
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Identify music playing around you.
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/video.txt:
--------------------------------------------------------------------------------
1 | https://github.com/user-attachments/assets/2ea0e79a-65d5-4560-826b-b53a57fc9a51.mp4
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.utils
2 |
3 | const val TAG = "Audire"
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/01.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/02.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/03.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/04.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/08.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexmercerind/audire/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/08.jpg
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Genres.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Genres(
7 | @SerializedName("primary")
8 | val primary: String?
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/ShazamResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ShazamResponse(
7 | @SerializedName("track")
8 | val track: Track?
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/repository/IdentifyRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.repository
2 |
3 | import com.alexmercerind.audire.models.Music
4 |
5 | abstract class IdentifyRepository {
6 | abstract suspend fun identify(duration: Int, data: ByteArray): Music?
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Aug 03 17:34:54 IST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Metadata.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Metadata(
7 | @SerializedName("text")
8 | val text: String?,
9 | @SerializedName("title")
10 | val title: String?
11 | )
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 |
Audire identifies the music playing near you and shows you the details. The app also saves a history of songs it identified for you in the past, so you can look up all songs later at your convenience as well.
For identification, Audire currently uses the APIs of Shazam.
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_stop_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/mappers/ByteArray.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.mappers
2 |
3 | fun ByteArray.toShortArray(): ShortArray {
4 | val result = ShortArray(size / 2)
5 | for (i in 0..result.size step 2) {
6 | result[i / 2] = (this[i].toInt() and 0xFF or (this[i + 1].toInt() shl 8)).toShort()
7 | }
8 | return result
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/mappers/HistoryItem.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.mappers
2 |
3 | import com.alexmercerind.audire.models.HistoryItem
4 | import com.alexmercerind.audire.models.Music
5 |
6 | fun HistoryItem.toMusic() = Music(
7 | title,
8 | artists,
9 | cover,
10 | album,
11 | label,
12 | year,
13 | lyrics
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/models/Music.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.models
2 |
3 | import java.io.Serializable
4 |
5 | data class Music(
6 | val title: String,
7 | val artists: String,
8 | val cover: String,
9 | val album: String?,
10 | val label: String?,
11 | val year: String?,
12 | val lyrics: String?
13 | ) : Serializable
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_file_download_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_file_upload_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Signature.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Signature(
7 | @SerializedName("samplems")
8 | val samplems: Int,
9 | @SerializedName("timestamp")
10 | val timestamp: Int,
11 | @SerializedName("uri")
12 | val uri: String
13 | )
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Audire"
17 | include(":app")
18 |
--------------------------------------------------------------------------------
/.idea/deviceManager.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Section.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Section(
7 | @SerializedName("metadata")
8 | val metadata: List?,
9 | @SerializedName("text")
10 | val text: List?,
11 | @SerializedName("type")
12 | val type: String?,
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/Audire.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire
2 |
3 | import android.app.Application
4 | import com.alexmercerind.audire.repository.SettingsRepository
5 |
6 | class Audire : Application() {
7 | override fun onCreate() {
8 | super.onCreate()
9 |
10 | // Apply current settings from SettingsRepository.
11 | SettingsRepository(this).apply()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Geolocation.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Geolocation(
7 | @SerializedName("altitude")
8 | val altitude: Double,
9 | @SerializedName("latitude")
10 | val latitude: Double,
11 | @SerializedName("longitude")
12 | val longitude: Double
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Images.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Images(
7 | @SerializedName("background")
8 | val background: String?,
9 | @SerializedName("coverart")
10 | val coverart: String?,
11 | @SerializedName("coverarthq")
12 | val coverarthq: String?,
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_clear_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_graphic_eq_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_info_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_music_note_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/x.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outline_description_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_calendar_today_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #00000000
5 | #FFFFFFFF
6 | #88000000
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_label_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_favorite_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/ShazamRequestBody.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ShazamRequestBody(
7 | @SerializedName("geolocation")
8 | val geolocation: Geolocation,
9 | @SerializedName("signature")
10 | val signature: Signature,
11 | @SerializedName("timestamp")
12 | val timestamp: Int,
13 | @SerializedName("timezone")
14 | val timezone: String
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_mic_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/models/Track.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam.models
2 |
3 |
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Track(
7 | @SerializedName("genres")
8 | val genres: Genres?,
9 | @SerializedName("images")
10 | val images: Images?,
11 | @SerializedName("sections")
12 | val sections: List?,
13 | @SerializedName("subtitle")
14 | val subtitle: String?,
15 | @SerializedName("title")
16 | val title: String?,
17 | )
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_album_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outline_person_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_search_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_history_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outline_lock_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/native/ShazamSignature.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.native
2 |
3 | // This class provides JNI binding to Shazam's signature algorithm.
4 | //
5 | // ShazamSignature.create takes audio samples as ShortArray.
6 | // Format: PCM 16 Bit LE
7 | // Sample Rate: 16000 Hz
8 | //
9 | // References:
10 | // https://github.com/marin-m/SongRec
11 | // https://github.com/alexmercerind/shazam-signature-jni
12 | class ShazamSignature {
13 | init {
14 | System.loadLibrary("shazam_signature_jni")
15 | }
16 |
17 | external fun create(input: ShortArray): String
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_favorite_border_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/identify_material_toolbar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_share_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_navigation_view_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
12 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/IdentifyViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.alexmercerind.audire.audio.AudioIdentifier
6 | import com.alexmercerind.audire.audio.AudioRecorder
7 | import com.alexmercerind.audire.repository.ShazamIdentifyRepository
8 |
9 | class IdentifyViewModel : ViewModel() {
10 | private val audioRecorder = AudioRecorder(viewModelScope)
11 | private val audioIdentifier = AudioIdentifier(viewModelScope, audioRecorder, ShazamIdentifyRepository())
12 | val error = audioIdentifier.error
13 | val music = audioIdentifier.music
14 | val duration = audioRecorder.duration
15 | val active = audioRecorder.active
16 |
17 | fun start() = audioRecorder.start()
18 |
19 | fun stop() = audioRecorder.stop()
20 |
21 | override fun onCleared() {
22 | super.onCleared()
23 | stop()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/mappers/Music.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.mappers
2 |
3 | import com.alexmercerind.audire.models.HistoryItem
4 | import com.alexmercerind.audire.models.Music
5 | import java.util.Calendar
6 |
7 | fun Music.toHistoryItem() = HistoryItem(
8 | null,
9 | Calendar.getInstance().time.time,
10 | title,
11 | artists,
12 | cover,
13 | album,
14 | label,
15 | year,
16 | lyrics,
17 | false
18 | )
19 |
20 | fun Music.toSearchQuery() = listOfNotNull(
21 | title.ifBlank { null },
22 | artists.ifBlank { null },
23 | album?.ifBlank { null },
24 | year?.ifBlank { null }
25 | )
26 | .joinToString(" ")
27 | .trim()
28 |
29 | fun Music.toShareText() = buildString {
30 | append(title)
31 | if (artists.isNotBlank()) {
32 | append("\n$artists")
33 | }
34 | if (!album.isNullOrBlank()) {
35 | append("\n$album")
36 | }
37 | if (!year.isNullOrBlank()) {
38 | append(" ($year)")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/history_material_toolbar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/db/HistoryItemDao.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.alexmercerind.audire.models.HistoryItem
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface HistoryItemDao {
13 | @Insert(onConflict = OnConflictStrategy.REPLACE)
14 | suspend fun insert(historyItem: HistoryItem)
15 |
16 | @Delete
17 | suspend fun delete(historyItem: HistoryItem)
18 |
19 | @Query("SELECT * FROM history_item ORDER BY timestamp DESC")
20 | fun getAll(): Flow>
21 |
22 | @Query("SELECT * FROM history_item WHERE LOWER(title) LIKE '%' || :term || '%' ORDER BY timestamp DESC")
23 | suspend fun search(term: String): List
24 |
25 | @Query("UPDATE history_item SET liked = 1 WHERE id = :id")
26 | suspend fun like(id: Int)
27 |
28 | @Query("UPDATE history_item SET liked = 0 WHERE id = :id")
29 | suspend fun unlike(id: Int)
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/repository/HistoryRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.repository
2 |
3 | import android.app.Application
4 | import com.alexmercerind.audire.db.HistoryItemDatabase
5 | import com.alexmercerind.audire.models.HistoryItem
6 |
7 | class HistoryRepository(private val application: Application) {
8 | suspend fun insert(historyItem: HistoryItem) =
9 | HistoryItemDatabase(application).historyItemDao().insert(historyItem)
10 |
11 | suspend fun delete(historyItem: HistoryItem) =
12 | HistoryItemDatabase(application).historyItemDao().delete(historyItem)
13 |
14 | fun getAll() = HistoryItemDatabase(application).historyItemDao().getAll()
15 |
16 | suspend fun search(term: String) =
17 | HistoryItemDatabase(application).historyItemDao().search(term)
18 |
19 | suspend fun like(historyItem: HistoryItem) =
20 | HistoryItemDatabase(application).historyItemDao().like(historyItem.id!!)
21 |
22 | suspend fun unlike(historyItem: HistoryItem) =
23 | HistoryItemDatabase(application).historyItemDao().unlike(historyItem.id!!)
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/models/HistoryItem.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.models
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import com.google.gson.annotations.SerializedName
7 | import java.io.Serializable
8 |
9 | @Entity(tableName = "history_item")
10 | data class HistoryItem(
11 | @SerializedName("id") @PrimaryKey(autoGenerate = true) val id: Int?,
12 | @SerializedName("timestamp") @ColumnInfo(name = "timestamp") val timestamp: Long,
13 | @SerializedName("title") @ColumnInfo(name = "title") val title: String,
14 | @SerializedName("artists") @ColumnInfo(name = "artists") val artists: String,
15 | @SerializedName("cover") @ColumnInfo(name = "cover") val cover: String,
16 | @SerializedName("album") @ColumnInfo(name = "album") val album: String?,
17 | @SerializedName("label") @ColumnInfo(name = "label") val label: String?,
18 | @SerializedName("year") @ColumnInfo(name = "year") val year: String?,
19 | @SerializedName("lyrics") @ColumnInfo(name = "lyrics") val lyrics: String?,
20 | @SerializedName("liked") @ColumnInfo(name = "liked", defaultValue = "0") val liked: Boolean
21 | ) : Serializable
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v23/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/db/HistoryItemDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.db
2 |
3 | import android.app.Application
4 | import androidx.room.AutoMigration
5 | import androidx.room.Database
6 | import androidx.room.Room
7 | import androidx.room.RoomDatabase
8 | import com.alexmercerind.audire.models.HistoryItem
9 |
10 | @Database(
11 | entities = [HistoryItem::class],
12 | version = 2,
13 | exportSchema = true,
14 | autoMigrations = [AutoMigration(from = 1, to = 2)]
15 | )
16 | abstract class HistoryItemDatabase : RoomDatabase() {
17 | abstract fun historyItemDao(): HistoryItemDao
18 |
19 | companion object {
20 | @Volatile
21 | private var instance: HistoryItemDatabase? = null
22 | private val lock = Any()
23 | operator fun invoke(application: Application) = instance ?: synchronized(lock) {
24 | instance ?: createDatabase(application).also { instance = it }
25 | }
26 |
27 | private fun createDatabase(application: Application) =
28 | Room.databaseBuilder(
29 | application,
30 | HistoryItemDatabase::class.java,
31 | "history-item-database"
32 | ).build()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/mappers/ShazamResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.mappers
2 |
3 | import com.alexmercerind.audire.api.shazam.models.ShazamResponse
4 | import com.alexmercerind.audire.models.Music
5 |
6 | fun ShazamResponse.toMusic() = track?.let {
7 | Music(
8 | track.title!!,
9 | track.subtitle!!,
10 | track.images?.coverarthq!!,
11 | track.sections
12 | ?.firstOrNull { section -> section.type?.uppercase() == "SONG" }
13 | ?.metadata
14 | ?.firstOrNull { metadata -> metadata.title?.uppercase() == "ALBUM" }
15 | ?.text,
16 | track.sections
17 | ?.firstOrNull { section -> section.type?.uppercase() == "SONG" }
18 | ?.metadata
19 | ?.firstOrNull { metadata -> metadata.title?.uppercase() == "LABEL" }
20 | ?.text,
21 | track.sections
22 | ?.firstOrNull { section -> section.type?.uppercase() == "SONG" }
23 | ?.metadata
24 | ?.firstOrNull { metadata -> metadata.title?.uppercase() == "RELEASED" }
25 | ?.text,
26 | track.sections
27 | ?.firstOrNull { section -> section.type?.uppercase() == "LYRICS" }
28 | ?.text
29 | ?.joinToString("\n")
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/repository/ImportExportRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.repository
2 |
3 | import android.app.Application
4 | import android.net.Uri
5 | import com.alexmercerind.audire.models.HistoryItem
6 | import com.google.gson.Gson
7 | import com.google.gson.reflect.TypeToken
8 |
9 | class ImportExportRepository(private val application: Application) {
10 | suspend fun import(uri: Uri) {
11 | val input = application.contentResolver.openInputStream(uri)
12 | input?.use {
13 | val json = it.readBytes().toString(Charsets.UTF_16)
14 | val historyItems: List = gson.fromJson(
15 | json, object : TypeToken>() {}.type
16 | )
17 | for (historyItem in historyItems) {
18 | HistoryRepository(application).insert(historyItem)
19 | }
20 | }
21 | }
22 |
23 | suspend fun export(uri: Uri, historyItems: List) {
24 | val output = application.contentResolver.openOutputStream(uri)
25 | output?.use {
26 | val json = gson.toJson(historyItems)
27 | it.write(json.toByteArray(Charsets.UTF_16))
28 | }
29 | }
30 |
31 | companion object {
32 | private val gson by lazy { Gson() }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/api/shazam/ShazamAPI.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.api.shazam
2 |
3 | import com.alexmercerind.audire.api.shazam.models.ShazamRequestBody
4 | import com.alexmercerind.audire.api.shazam.models.ShazamResponse
5 | import retrofit2.Response
6 | import retrofit2.http.Body
7 | import retrofit2.http.Header
8 | import retrofit2.http.POST
9 | import retrofit2.http.Path
10 | import retrofit2.http.Query
11 |
12 | interface ShazamAPI {
13 | @POST("discovery/v5/en/US/android/-/tag/{uuidDNS}/{uuidURL}")
14 | suspend fun discovery(
15 | @Body body: ShazamRequestBody,
16 | @Path("uuidDNS") uuidDNS: String,
17 | @Path("uuidURL") uuidURL: String,
18 | @Header("User-Agent") userAgent: String,
19 | @Header("Content-Language") contentLanguage: String = "en_US",
20 | @Header("Content-Type") contentType: String = "application/json",
21 | @Query("sync") sync: String = "true",
22 | @Query("webv3") webv3: String = "true",
23 | @Query("sampling") sampling: String = "true",
24 | @Query("connected") connected: String = "",
25 | @Query("shazamapiversion") shazamapiversion: String = "v3",
26 | @Query("sharehub") sharehub: String = "true",
27 | @Query("video") video: String = "v3",
28 | ): Response
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/services/FindMusicTileService.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.services
2 |
3 | import android.app.PendingIntent
4 | import android.content.Intent
5 | import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
6 | import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
7 | import android.os.Build
8 | import android.service.quicksettings.TileService
9 | import androidx.annotation.RequiresApi
10 | import androidx.core.service.quicksettings.PendingIntentActivityWrapper
11 | import androidx.core.service.quicksettings.TileServiceCompat
12 | import com.alexmercerind.audire.ui.MainActivity
13 |
14 | @RequiresApi(Build.VERSION_CODES.N)
15 | class FindMusicTileService : TileService() {
16 | companion object {
17 | var handled = true
18 | }
19 |
20 | override fun onClick() {
21 | super.onClick()
22 | handled = false
23 |
24 | val intent = Intent(applicationContext, MainActivity::class.java).apply {
25 | flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP
26 | }
27 | val wrapper = PendingIntentActivityWrapper(
28 | applicationContext,
29 | 0,
30 | intent,
31 | PendingIntent.FLAG_CANCEL_CURRENT,
32 | false
33 | )
34 | TileServiceCompat.startActivityAndCollapse(this, wrapper)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
17 |
20 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/values-v29/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v29/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep,allowobfuscation,allowshrinking interface retrofit2.Call
2 | -keep,allowobfuscation,allowshrinking class retrofit2.Response
3 |
4 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
5 |
6 | -keep class com.alexmercerind.audire.api.**
7 |
8 | ##---------------Begin: proguard configuration for Gson ----------
9 | # Gson uses generic type information stored in a class file when working with fields. Proguard
10 | # removes such information by default, so configure it to keep all of it.
11 | -keepattributes Signature
12 |
13 | # For using GSON @Expose annotation
14 | -keepattributes *Annotation*
15 |
16 | # Gson specific classes
17 | -dontwarn sun.misc.**
18 | #-keep class com.google.gson.stream.** { *; }
19 |
20 | # Application classes that will be serialized/deserialized over Gson
21 | -keep class com.google.gson.examples.android.model.** { ; }
22 |
23 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
24 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
25 | -keep class * implements com.google.gson.TypeAdapter
26 | -keep class * implements com.google.gson.TypeAdapterFactory
27 | -keep class * implements com.google.gson.JsonSerializer
28 | -keep class * implements com.google.gson.JsonDeserializer
29 |
30 | # Prevent R8 from leaving Data object members always null
31 | -keepclassmembers,allowobfuscation class * {
32 | @com.google.gson.annotations.SerializedName ;
33 | }
34 |
35 | ##---------------End: proguard configuration for Gson ----------
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.app.Application
4 | import android.net.Uri
5 | import androidx.lifecycle.AndroidViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.alexmercerind.audire.repository.HistoryRepository
8 | import com.alexmercerind.audire.repository.ImportExportRepository
9 | import com.alexmercerind.audire.repository.SettingsRepository
10 | import kotlinx.coroutines.flow.first
11 | import kotlinx.coroutines.launch
12 |
13 | class SettingsViewModel(application: Application) : AndroidViewModel(application) {
14 | companion object {
15 | var autoStartHandled = false
16 | }
17 |
18 | private val historyRepository = HistoryRepository(application)
19 | private val settingsRepository = SettingsRepository(application)
20 | private val importExportRepository = ImportExportRepository(application)
21 |
22 | val autoStart = settingsRepository.autoStart
23 |
24 | val theme = settingsRepository.theme
25 |
26 | val systemColorScheme = settingsRepository.systemColorScheme
27 |
28 | fun setAutoStart(value: Boolean) = viewModelScope.launch { settingsRepository.setAutoStart(value) }
29 |
30 | fun setTheme(value: String) = viewModelScope.launch { settingsRepository.setTheme(value) }
31 |
32 | fun setSystemColorScheme(value: Boolean) = viewModelScope.launch { settingsRepository.setSystemColorScheme(value) }
33 |
34 | fun import(uri: Uri) = viewModelScope.launch { importExportRepository.import(uri) }
35 |
36 | fun export(uri: Uri) = viewModelScope.launch { importExportRepository.export(uri, historyRepository.getAll().first()) }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
23 |
24 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: Set up JDK 21
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: '21'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Set up JNI libraries
24 | run: |
25 | curl -L https://github.com/alexmercerind/shazam-signature-jni/releases/download/v1.0.1/jniLibs.zip -o jniLibs.zip
26 | unzip jniLibs.zip -d ./app/src/main
27 | - name: Grant execute permission for gradlew
28 | run: chmod +x gradlew
29 | - name: Build with Gradle
30 | run: ./gradlew build
31 | - name: Sign APK
32 | uses: r0adkll/sign-android-release@v1
33 | id: sign_app
34 | with:
35 | releaseDirectory: ./app/build/outputs/apk/release/
36 | signingKeyBase64: ${{ secrets.SIGNING_KEY }}
37 | alias: ${{ secrets.ALIAS }}
38 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
39 | keyPassword: ${{ secrets.KEY_PASSWORD }}
40 | env:
41 | BUILD_TOOLS_VERSION: "36.0.0"
42 | - name: Copy release APK
43 | run: cp ${{ steps.sign_app.outputs.signedReleaseFile }} ./app/build/outputs/apk/release/app-release-signed.apk
44 | - name: Upload artifact
45 | uses: actions/upload-artifact@v4
46 | with:
47 | name: app-release
48 | path: ./app/build/outputs/apk/release/app-release-signed.apk
49 | - name: GitHub release
50 | uses: softprops/action-gh-release@v1
51 | if: github.ref == 'refs/heads/main'
52 | with:
53 | draft: true
54 | prerelease: false
55 | tag_name: "vnext"
56 | token: ${{ secrets.ACCESS_TOKEN }}
57 | files: |
58 | ./app/build/outputs/apk/release/app-release-signed.apk
59 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/content_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
19 |
20 |
25 |
28 |
31 |
32 |
37 |
40 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import androidx.activity.enableEdgeToEdge
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.lifecycle.lifecycleScope
9 | import androidx.navigation.fragment.NavHostFragment
10 | import androidx.navigation.ui.setupWithNavController
11 | import com.alexmercerind.audire.R
12 | import com.alexmercerind.audire.databinding.ActivityMainBinding
13 | import com.alexmercerind.audire.services.FindMusicTileService
14 | import kotlinx.coroutines.launch
15 |
16 | class MainActivity : AppCompatActivity() {
17 | val identifyViewModel by viewModels()
18 | val settingsViewModel by viewModels()
19 |
20 | private lateinit var binding: ActivityMainBinding
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | enableEdgeToEdge()
25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
26 | window.isNavigationBarContrastEnforced = false
27 | }
28 | binding = ActivityMainBinding.inflate(layoutInflater)
29 | val view = binding.root
30 | setContentView(view)
31 | // https://stackoverflow.com/a/50537193/12825435
32 | val navHostFragment = supportFragmentManager.findFragmentById(R.id.content) as NavHostFragment
33 | binding.bottomNavigationView.setupWithNavController(navHostFragment.navController)
34 | }
35 |
36 | override fun onResume() {
37 | super.onResume()
38 | lifecycleScope.launch {
39 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
40 | if (!FindMusicTileService.handled || (!SettingsViewModel.autoStartHandled && settingsViewModel.autoStart.value)) {
41 | FindMusicTileService.handled = true
42 | SettingsViewModel.autoStartHandled = true
43 | identifyViewModel.start()
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/audio/AudioIdentifier.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.audio
2 |
3 | import com.alexmercerind.audire.models.Music
4 | import com.alexmercerind.audire.repository.IdentifyRepository
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.FlowPreview
7 | import kotlinx.coroutines.flow.MutableSharedFlow
8 | import kotlinx.coroutines.flow.asSharedFlow
9 | import kotlinx.coroutines.flow.combine
10 | import kotlinx.coroutines.flow.launchIn
11 | import kotlinx.coroutines.flow.onEach
12 | import kotlinx.coroutines.flow.sample
13 |
14 | @OptIn(FlowPreview::class)
15 | class AudioIdentifier(
16 | private val scope: CoroutineScope,
17 | private val audioRecorder: AudioRecorder,
18 | private val identifyRepository: IdentifyRepository
19 | ) {
20 | private val _error = MutableSharedFlow()
21 | private val _music = MutableSharedFlow()
22 | val error get() = _error.asSharedFlow()
23 | val music get() = _music.asSharedFlow()
24 |
25 | init {
26 | combine(audioRecorder.duration, audioRecorder.buffer) { duration, buffer -> duration to buffer }
27 | .sample(2000L)
28 | .onEach(::process)
29 | .launchIn(scope)
30 | }
31 |
32 | private suspend fun process(data: Pair) {
33 | val (duration, buffer) = data
34 | runCatching {
35 | if (buffer.isEmpty()) return
36 | if (duration < MIN_DURATION) return
37 | if (duration > MAX_DURATION) {
38 | _error.emit(Unit)
39 | audioRecorder.stop()
40 | return
41 | }
42 | identifyRepository.identify(duration, buffer)?.let {
43 | // HACK: Prevent obscure music from being displayed if duration lesser than MAX_DURATION.
44 | if (it.album.isNullOrEmpty() && duration < MAX_DURATION) return
45 | _music.emit(it)
46 | audioRecorder.stop()
47 | }
48 | }
49 | }
50 |
51 | companion object {
52 | const val MIN_DURATION = 2
53 | const val MAX_DURATION = 12
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Audire
4 |
5 | **🎵 Identify music playing around you.**
6 |
7 | https://github.com/user-attachments/assets/2ea0e79a-65d5-4560-826b-b53a57fc9a51.mp4
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## Download
27 |
28 | - [GitHub Releases](https://github.com/alexmercerind/audire/releases/latest)
29 | - [Google Play Store](https://play.google.com/store/apps/details?id=com.alexmercerind.audire)
30 | - [IzzySoft](https://apt.izzysoft.de/fdroid/index/apk/com.alexmercerind.audire)
31 |
32 | ## Building
33 |
34 | Refer to [CI](https://github.com/alexmercerind/audire/blob/main/.github/workflows/android.yml).
35 |
36 | ## Architecture
37 |
38 | The project uses MVVM & [Android Architecture Components](https://developer.android.com/topic/architecture).
39 |
40 | ## License
41 |
42 | 
43 |
44 | This project & the work under this repository is governed by GNU General Public License v3.0.
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/utils/WaveView.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.utils
2 |
3 | import android.animation.ValueAnimator
4 | import android.content.Context
5 | import android.graphics.*
6 | import android.util.AttributeSet
7 | import android.view.View
8 | import com.google.android.material.color.MaterialColors
9 | import kotlin.math.sin
10 |
11 | class WaveView @JvmOverloads constructor(
12 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
13 | ) : View(context, attrs, defStyleAttr) {
14 |
15 | private var amplitude = 30.0F.toDp() // scale
16 | private var animator: ValueAnimator? = null
17 | private var paint = Paint(Paint.ANTI_ALIAS_FLAG)
18 | private val path = Path()
19 | private var speed = 0.0F
20 |
21 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
22 | super.onSizeChanged(w, h, oldw, oldh)
23 | animator?.cancel()
24 | animator = createAnimator().apply { start() }
25 | }
26 |
27 | override fun onDraw(c: Canvas) = c.drawPath(path, paint)
28 |
29 | private fun createAnimator(): ValueAnimator {
30 | return ValueAnimator.ofFloat(0f, Float.MAX_VALUE).apply {
31 | repeatCount = ValueAnimator.INFINITE
32 | addUpdateListener {
33 | speed -= WAVE_SPEED
34 | createPath()
35 | invalidate()
36 | }
37 | }
38 | }
39 |
40 | private fun createPath() {
41 | path.reset()
42 | // https://stackoverflow.com/a/64509627/12825435
43 | paint.color = MaterialColors.getColor(
44 | context, com.google.android.material.R.attr.colorSurfaceVariant, Color.BLACK
45 | )
46 |
47 | path.moveTo(0f, height.toFloat())
48 | path.lineTo(0f, amplitude)
49 | for (i in 0..width step 10) {
50 | val x = i.toFloat()
51 | val y =
52 | sin((i + 10) * Math.PI / WAVE_AMOUNT_ON_SCREEN + speed).toFloat() * amplitude + amplitude * 2
53 | path.lineTo(x, y)
54 | }
55 | path.lineTo(width.toFloat(), height.toFloat())
56 | path.close()
57 | }
58 |
59 | override fun onDetachedFromWindow() {
60 | animator?.cancel()
61 | super.onDetachedFromWindow()
62 | }
63 |
64 | companion object {
65 | const val WAVE_SPEED = 0.1F
66 | const val WAVE_AMOUNT_ON_SCREEN = 300
67 | }
68 |
69 | private fun Float.toDp() = this * context.resources.displayMetrics.density
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/HistoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.alexmercerind.audire.models.HistoryItem
7 | import com.alexmercerind.audire.repository.HistoryRepository
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.flow.first
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.sync.Mutex
14 | import kotlinx.coroutines.sync.withLock
15 |
16 | class HistoryViewModel(application: Application) : AndroidViewModel(application) {
17 | val historyItems: StateFlow?>
18 | get() = _historyItems
19 |
20 | private val _historyItems = MutableStateFlow?>(null)
21 |
22 | var query: String = ""
23 | set(value) {
24 | // Avoid duplicate operation in Room.
25 | if (value == field) {
26 | return
27 | }
28 |
29 | field = value
30 | viewModelScope.launch {
31 | mutex.withLock {
32 | _historyItems.emit(
33 | when (value) {
34 | // Search query == "" -> Show all HistoryItem(s)
35 | "" -> getAll().first()
36 | // Search query != "" -> Show search HistoryItem(s)
37 | else -> search(value.lowercase())
38 | }
39 | )
40 | }
41 | }
42 | }
43 |
44 | private val mutex = Mutex()
45 |
46 | private val repository = HistoryRepository(application)
47 |
48 | init {
49 | viewModelScope.launch {
50 | getAll().collect {
51 | _historyItems.emit(it)
52 | }
53 | }
54 | }
55 |
56 | private fun getAll() = repository.getAll()
57 |
58 | private suspend fun search(query: String) = repository.search(query)
59 |
60 | fun insert(historyItem: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { repository.insert(historyItem) }
61 |
62 | fun delete(historyItem: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { repository.delete(historyItem) }
63 |
64 | fun like(historyItem: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { repository.like(historyItem) }
65 |
66 | fun unlike(historyItem: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { repository.unlike(historyItem) }
67 | }
68 |
--------------------------------------------------------------------------------
/app/schemas/com.alexmercerind.audire.db.HistoryItemDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "8d68b1ca8c519f81965c7af7229d1406",
6 | "entities": [
7 | {
8 | "tableName": "history_item",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `artists` TEXT NOT NULL, `cover` TEXT NOT NULL, `album` TEXT, `label` TEXT, `year` TEXT, `lyrics` TEXT)",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": false
16 | },
17 | {
18 | "fieldPath": "timestamp",
19 | "columnName": "timestamp",
20 | "affinity": "INTEGER",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "title",
25 | "columnName": "title",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "artists",
31 | "columnName": "artists",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "cover",
37 | "columnName": "cover",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "album",
43 | "columnName": "album",
44 | "affinity": "TEXT",
45 | "notNull": false
46 | },
47 | {
48 | "fieldPath": "label",
49 | "columnName": "label",
50 | "affinity": "TEXT",
51 | "notNull": false
52 | },
53 | {
54 | "fieldPath": "year",
55 | "columnName": "year",
56 | "affinity": "TEXT",
57 | "notNull": false
58 | },
59 | {
60 | "fieldPath": "lyrics",
61 | "columnName": "lyrics",
62 | "affinity": "TEXT",
63 | "notNull": false
64 | }
65 | ],
66 | "primaryKey": {
67 | "autoGenerate": true,
68 | "columnNames": [
69 | "id"
70 | ]
71 | },
72 | "indices": [],
73 | "foreignKeys": []
74 | }
75 | ],
76 | "views": [],
77 | "setupQueries": [
78 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
79 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d68b1ca8c519f81965c7af7229d1406')"
80 | ]
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/AboutActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.os.Bundle
7 | import android.view.View
8 | import androidx.activity.enableEdgeToEdge
9 | import com.alexmercerind.audire.databinding.ActivityAboutBinding
10 | import androidx.core.net.toUri
11 |
12 | class AboutActivity : AppCompatActivity() {
13 | companion object {
14 | private const val GITHUB = "https://github.com/alexmercerind/audire"
15 | private const val LICENSE = "https://github.com/alexmercerind/audire/blob/main/LICENSE"
16 | private const val PRIVACY = "https://github.com/alexmercerind/audire/wiki/Privacy-Policy-%5BPlay-Store%5D"
17 |
18 | private const val DEVELOPER_GITHUB = "https://github.com/alexmercerind"
19 | private const val DEVELOPER_X = "https://x.com/alexmercerind"
20 | }
21 |
22 | private lateinit var binding: ActivityAboutBinding
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | enableEdgeToEdge()
27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
28 | window.isNavigationBarContrastEnforced = false
29 | }
30 | binding = ActivityAboutBinding.inflate(layoutInflater)
31 | val view = binding.root
32 | setContentView(view)
33 |
34 | try {
35 | val info = packageManager.getPackageInfo(packageName, 0)
36 | binding.descriptionTextView.text = listOf(
37 | info.versionName,
38 | "(${if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) info.versionCode else info.longVersionCode})"
39 | ).joinToString(" ")
40 | } catch (e: Throwable) {
41 | e.printStackTrace()
42 | }
43 |
44 | binding.materialToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
45 | setOnClickListener(binding.githubLinearLayout, GITHUB)
46 | setOnClickListener(binding.licenseLinearLayout, LICENSE)
47 | setOnClickListener(binding.privacyLinearLayout, PRIVACY)
48 | setOnClickListener(binding.developerGitHubLinearLayout, DEVELOPER_GITHUB)
49 | setOnClickListener(binding.developerXLinearLayout, DEVELOPER_X)
50 | }
51 |
52 | private fun setOnClickListener(view: View, uri: String) {
53 | view.setOnClickListener {
54 | try {
55 | val intent = Intent(Intent.ACTION_VIEW, uri.toUri())
56 | startActivity(intent)
57 | } catch (e: Throwable) {
58 | e.printStackTrace()
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | id("com.android.application")
5 | id("org.jetbrains.kotlin.android")
6 | id("com.google.devtools.ksp")
7 | }
8 |
9 | android {
10 | namespace = "com.alexmercerind.audire"
11 | compileSdk = 36
12 |
13 | defaultConfig {
14 | applicationId = "com.alexmercerind.audire"
15 | minSdk = 21
16 | targetSdk = 36
17 | versionCode = 3
18 | versionName = "1.2"
19 | }
20 |
21 | buildFeatures {
22 | viewBinding = true
23 | }
24 |
25 | buildTypes {
26 | release {
27 | isDebuggable = false
28 | isMinifyEnabled = true
29 | proguardFiles(
30 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
31 | )
32 | }
33 | debug {
34 | isDebuggable = true
35 | }
36 | }
37 | compileOptions {
38 | isCoreLibraryDesugaringEnabled = true
39 | sourceCompatibility = JavaVersion.VERSION_21
40 | targetCompatibility = JavaVersion.VERSION_21
41 | }
42 | kotlin {
43 | compilerOptions {
44 | jvmTarget = JvmTarget.JVM_21
45 | }
46 | }
47 |
48 | ksp {
49 | arg("room.schemaLocation", "$projectDir/schemas")
50 | }
51 |
52 | lint {
53 | disable.add("MissingTranslation")
54 | }
55 | }
56 |
57 | dependencies {
58 | implementation("androidx.core:core-ktx:1.16.0")
59 | implementation("androidx.appcompat:appcompat:1.7.1")
60 | implementation("com.google.android.material:material:1.14.0-alpha06")
61 | implementation("androidx.constraintlayout:constraintlayout:2.2.1")
62 |
63 | val navVersion = "2.9.3"
64 | implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
65 | implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
66 |
67 | val roomVersion = "2.7.2"
68 | implementation("androidx.room:room-ktx:$roomVersion")
69 | implementation("androidx.room:room-runtime:$roomVersion")
70 | annotationProcessor("androidx.room:room-compiler:$roomVersion")
71 | ksp("androidx.room:room-compiler:$roomVersion")
72 |
73 | implementation("com.google.code.gson:gson:2.13.1")
74 | implementation("com.squareup.retrofit2:retrofit:3.0.0")
75 | implementation("com.squareup.retrofit2:converter-gson:3.0.0")
76 | implementation("com.squareup.okhttp3:okhttp:5.1.0")
77 | implementation("com.squareup.okhttp3:logging-interceptor:5.1.0")
78 |
79 | implementation("com.github.f4b6a3:uuid-creator:6.1.1")
80 |
81 | implementation("io.coil-kt:coil:2.7.0")
82 |
83 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
21 |
24 |
27 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
56 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/schemas/com.alexmercerind.audire.db.HistoryItemDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "2ea2e6c994114db4cc6a40d4382db78c",
6 | "entities": [
7 | {
8 | "tableName": "history_item",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `artists` TEXT NOT NULL, `cover` TEXT NOT NULL, `album` TEXT, `label` TEXT, `year` TEXT, `lyrics` TEXT, `liked` INTEGER NOT NULL DEFAULT 0)",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": false
16 | },
17 | {
18 | "fieldPath": "timestamp",
19 | "columnName": "timestamp",
20 | "affinity": "INTEGER",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "title",
25 | "columnName": "title",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "artists",
31 | "columnName": "artists",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "cover",
37 | "columnName": "cover",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "album",
43 | "columnName": "album",
44 | "affinity": "TEXT",
45 | "notNull": false
46 | },
47 | {
48 | "fieldPath": "label",
49 | "columnName": "label",
50 | "affinity": "TEXT",
51 | "notNull": false
52 | },
53 | {
54 | "fieldPath": "year",
55 | "columnName": "year",
56 | "affinity": "TEXT",
57 | "notNull": false
58 | },
59 | {
60 | "fieldPath": "lyrics",
61 | "columnName": "lyrics",
62 | "affinity": "TEXT",
63 | "notNull": false
64 | },
65 | {
66 | "fieldPath": "liked",
67 | "columnName": "liked",
68 | "affinity": "INTEGER",
69 | "notNull": true,
70 | "defaultValue": "0"
71 | }
72 | ],
73 | "primaryKey": {
74 | "autoGenerate": true,
75 | "columnNames": [
76 | "id"
77 | ]
78 | },
79 | "indices": [],
80 | "foreignKeys": []
81 | }
82 | ],
83 | "views": [],
84 | "setupQueries": [
85 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
86 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ea2e6c994114db4cc6a40d4382db78c')"
87 | ]
88 | }
89 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/history_item.xml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
18 |
19 |
23 |
24 |
31 |
32 |
39 |
40 |
47 |
48 |
49 |
54 |
55 |
65 |
66 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-v28/history_item.xml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
18 |
19 |
23 |
24 |
31 |
32 |
39 |
40 |
47 |
48 |
49 |
54 |
55 |
65 |
66 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/adapters/HistoryItemAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui.adapters
2 |
3 | import android.content.Intent
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import coil.ImageLoader
9 | import coil.load
10 | import coil.request.CachePolicy
11 | import com.alexmercerind.audire.R
12 | import com.alexmercerind.audire.databinding.HistoryItemBinding
13 | import com.alexmercerind.audire.mappers.toMusic
14 | import com.alexmercerind.audire.models.HistoryItem
15 | import com.alexmercerind.audire.ui.HistoryViewModel
16 | import com.alexmercerind.audire.ui.MusicActivity
17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
18 |
19 | class HistoryItemAdapter(
20 | var items: List,
21 | private val historyViewModel: HistoryViewModel,
22 | ) : RecyclerView.Adapter() {
23 |
24 | inner class HistoryItemViewHolder(val binding: HistoryItemBinding) : RecyclerView.ViewHolder(binding.root)
25 |
26 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = HistoryItemViewHolder(
27 | HistoryItemBinding.inflate(
28 | LayoutInflater.from(parent.context),
29 | parent,
30 | false
31 | )
32 | )
33 |
34 | override fun getItemCount() = items.size
35 |
36 | override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) {
37 | holder.binding.apply {
38 | val context = root.context
39 | coverImageView.load(
40 | items[position].cover,
41 | ImageLoader.Builder(context).memoryCachePolicy(CachePolicy.ENABLED)
42 | .diskCachePolicy(CachePolicy.ENABLED).build()
43 | ) {
44 | crossfade(true)
45 | }
46 | titleTextView.text = items[position].title
47 | artistTextView.text = items[position].artists
48 | root.isLongClickable = true
49 | root.setOnClickListener {
50 | Intent(context, MusicActivity::class.java).also {
51 | it.putExtra(MusicActivity.MUSIC, items[position].toMusic())
52 | context.startActivity(it)
53 | }
54 | }
55 | root.setOnLongClickListener {
56 | MaterialAlertDialogBuilder(
57 | root.context, R.style.Base_Theme_Audire_MaterialAlertDialog
58 | ).setTitle(R.string.remove_history_item_title).setMessage(
59 | context.getString(
60 | R.string.remove_history_item_message, items[position].title
61 | )
62 | ).setPositiveButton(R.string.yes) { dialog, _ ->
63 | dialog.dismiss()
64 | historyViewModel.delete(items[position])
65 | }.setNegativeButton(R.string.no) { dialog, _ ->
66 | dialog.dismiss()
67 | }.show()
68 | true
69 | }
70 |
71 | likedImageView.visibility = if (items[position].liked) View.VISIBLE else View.GONE
72 | unlikedImageView.visibility = if (!items[position].liked) View.VISIBLE else View.GONE
73 |
74 | likeFrameLayout.setOnClickListener {
75 | if (items[position].liked) {
76 | historyViewModel.unlike(items[position])
77 | } else {
78 | historyViewModel.like(items[position])
79 | }
80 | }
81 | }
82 |
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/res/values-cs/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | O aplikaci
3 | Aplikaci se nepodařilo spustit
4 | Album
5 | Audire
6 | Historie
7 | Rozpoznat
8 | Obal
9 | Vývojář
10 | Sledovat na GitHubu
11 | Hitesh Kumar Saini
12 | Sledovat na X
13 | Vyhledat hudbu
14 | GitHub
15 | Nepodařilo se rozpoznat. Opakovat pokus?
16 | :(
17 | Podrobnosti
18 | Nahrávání
19 | Aplikace nemá přístup k mikrofonu, aby mohla nahrávat zvuk. Zajistěte přístup k oprávnění android.permission.RECORD_AUDIO.
20 | Nedostatečná oprávnění
21 | Stop
22 | Štítek
23 | Licence
24 | Oblíbené
25 | Písňové texty
26 | Ne
27 | Zde se zobrazí vaše historie
28 | Nic takového nebylo nalezeno
29 | OK
30 | Soukromí
31 | Chcete odstranit \"%1$s\" z historie?
32 | Odstranit
33 | Hledat
34 | Nastavení
35 | Vzhled
36 | Barevné schéma systému
37 | Motiv
38 | Tmavý
39 | Světlý
40 | Systémový
41 | Je vyžadován restart aplikace
42 | Záloha
43 | Exportovat historii
44 | Export selhal
45 | Export úspěšný
46 | Zálohování historie do souboru JSON
47 | Historie
48 | Importovat historii
49 | Import selhal
50 | Import úspěšný
51 | Obnovení historie ze souboru JSON
52 | Spotify
53 | Rok
54 | Ano
55 | YouTube
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Info
3 | Impossibile avviare l\'app
4 | Album
5 | Audire
6 | Cronologia
7 | Identifica
8 | Copertina
9 | Sviluppatore
10 | Segui su GitHub
11 | Hitesh Kumar Saini
12 | Segui su X
13 | Trova musica
14 | GitHub
15 | Non identificata. Riprovare?
16 | :(
17 | Dettagli
18 | Registra
19 | L\'app non può accedere al microfono per registrare l\'audio. Concedi l\'accesso al permesso android.permission.RECORD_AUDIO.
20 | Permessi insufficienti
21 | Stop
22 | Etichetta
23 | Licenza
24 | Preferiti
25 | Testo
26 | No
27 | La cronologia sarà mostrata qui
28 | Niente di simile trovato
29 | OK
30 | Privacy
31 | Vuoi rimuovere \"%1$s\" dalla cronologia?
32 | Rimuovi
33 | Cerca
34 | Impostazioni
35 | Aspetto
36 | Schema colori di sistema
37 | Tema
38 | Scuro
39 | Chiaro
40 | Sistema
41 | Necessario riavvio app
42 | Backup
43 | Esporta cronologia
44 | Esportazione fallita
45 | Esportazione completata
46 | Backup cronologia su file JSON
47 | Cronologia
48 | Importa cronologia
49 | Importazione fallita
50 | Importazione riuscita
51 | Ripristina cronologia da file JSON
52 | Spotify
53 | Anno
54 | Sì
55 | YouTube
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | À propos
3 | Impossible de démarrer l\'application
4 | Album
5 | Audire
6 | Historique
7 | Identifier
8 | Couverture
9 | Développeur
10 | Suivre sur GitHub
11 | Hitesh Kumar Saini
12 | Suivre sur X
13 | Chercher la musique
14 | GitHub
15 | Identification impossible. Recommencer ?
16 | :(
17 | Détails
18 | Enregistrer
19 | L\'application ne peut pas accéder à votre microphone pour enregistrer l\'audio. Il faut fournir l\'accès à l\'autorisation android.permission.RECORD_AUDIO.
20 | Permissions insuffisantes
21 | Arrêter
22 | Étiquette
23 | Licence
24 | Aimé
25 | Paroles
26 | Non
27 | Votre historique sera affiché ici
28 | Aucun résultat trouvé
29 | Ok
30 | Confidentialité
31 | Voulez-vous \"%1$s\" depuis l\'historique ?
32 | Retirer
33 | Rechercher
34 | Paramètres
35 | Apparence
36 | Schéma de couleurs du système
37 | Thème
38 | Sombre
39 | Lumineux
40 | Système
41 | Le redémarrage de l\'application est requis
42 | Sauvegarde
43 | Exporter l\'historique
44 | Échec de l\'exportation
45 | Exportation réussie
46 | Sauvegarder l\'historique dans un fichier JSON
47 | Historique
48 | Importer l\'historique
49 | Échec lors de l\'importation
50 | Importation réussie
51 | Restaurer l\'historique à partir d\'un fichier JSON
52 | Spotify
53 | Année
54 | Oui
55 | YouTube
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/audio/AudioRecorder.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.audio
2 |
3 | import android.media.AudioFormat
4 | import android.media.AudioRecord
5 | import android.media.MediaRecorder
6 | import android.os.Process
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.Job
10 | import kotlinx.coroutines.cancelAndJoin
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.isActive
14 | import kotlinx.coroutines.launch
15 | import kotlinx.coroutines.sync.Mutex
16 | import kotlinx.coroutines.sync.withLock
17 | import kotlin.coroutines.coroutineContext
18 | import kotlin.math.max
19 |
20 | class AudioRecorder(private val scope: CoroutineScope) {
21 | private var instance: AudioRecord? = null
22 | private var job: Job? = null
23 | private val mutex = Mutex()
24 | private val _active = MutableStateFlow(false)
25 | private val _duration = MutableStateFlow(0)
26 | private val _buffer = MutableStateFlow(ByteArray(0))
27 | val active get() = _active.asStateFlow()
28 | val duration get() = _duration.asStateFlow()
29 | val buffer get() = _buffer.asStateFlow()
30 |
31 |
32 | @Throws(SecurityException::class)
33 | fun start() {
34 | scope.launch {
35 | mutex.withLock {
36 | if (_active.value) return@launch
37 | instance = AudioRecord(
38 | MediaRecorder.AudioSource.MIC,
39 | SAMPLE_RATE,
40 | CHANNEL_CONFIG,
41 | AUDIO_FORMAT,
42 | BUFFER_SIZE
43 | )
44 | instance?.startRecording()
45 | reset(true)
46 | job = scope.launch(Dispatchers.IO) { loop() }
47 | }
48 | }
49 | }
50 |
51 | fun stop() {
52 | scope.launch {
53 | mutex.withLock {
54 | if (!_active.value) return@launch
55 | job?.cancelAndJoin()
56 | instance?.stop()
57 | instance?.release()
58 | instance = null
59 | reset(false)
60 | }
61 | }
62 | }
63 |
64 | private suspend fun loop() {
65 | Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)
66 | runCatching {
67 | val result = mutableListOf()
68 | while (coroutineContext.isActive) {
69 | val duration = result.size / (SAMPLE_RATE * SAMPLE_WIDTH * CHANNEL_COUNT)
70 | val buffer = ByteArray(BUFFER_SIZE)
71 | instance?.read(buffer, 0, buffer.size)
72 | result.addAll(buffer.toList())
73 | _duration.emit(duration)
74 | _buffer.emit(result.toByteArray())
75 | }
76 | }.onFailure {
77 | it.printStackTrace()
78 | reset(false)
79 | }
80 | }
81 |
82 | private suspend fun reset(active: Boolean) {
83 | _active.emit(active)
84 | _duration.emit(0)
85 | _buffer.emit(ByteArray(0))
86 | }
87 |
88 | companion object {
89 | private const val SAMPLE_RATE = 16000
90 | private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
91 | private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
92 | private const val CHANNEL_COUNT = 1
93 | private const val SAMPLE_WIDTH = 2
94 | private const val BUFFER_SIZE_MULTIPLIER = 8
95 | private val BUFFER_SIZE = max(
96 | AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) * BUFFER_SIZE_MULTIPLIER,
97 | SAMPLE_RATE * SAMPLE_WIDTH * CHANNEL_COUNT
98 | )
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | About
3 | Couldn\'t start the application
4 | Album
5 | Audire
6 | Artist
7 | History
8 | Identify
9 | Cover
10 | Developer
11 | Follow on GitHub
12 | Hitesh Kumar Saini
13 | Follow on X
14 | Find music
15 | GitHub
16 | Could not identify. Retry?
17 | :(
18 | Details
19 | Record
20 | The application cannot access your microphone to record the audio. Provide access to android.permission.RECORD_AUDIO permission.
21 | Insufficient permissions
22 | Stop
23 | Label
24 | License
25 | Liked
26 | Lyrics
27 | No
28 | Your history will be displayed here
29 | Nothing like that was found
30 | OK
31 | Privacy
32 | Do you want to remove \"%1$s\" from the history?
33 | Remove
34 | Search
35 | Settings
36 | Appearance
37 | System color scheme
38 | Theme
39 | Dark
40 | Light
41 | System
42 | Application restart is required
43 | Backup
44 | Export history
45 | Export failed
46 | Export successful
47 | Backup history to JSON file
48 | application/json
49 | History
50 | Import history
51 | Import failed
52 | Import successful
53 | Restore history from JSON file
54 | Behavior
55 | Identify on app launch
56 | Spotify
57 | Year
58 | Yes
59 | YouTube
60 | Share
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_identify.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
32 |
33 |
34 |
39 |
40 |
50 |
51 |
68 |
69 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/repository/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.repository
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.os.Build
6 | import androidx.appcompat.app.AppCompatDelegate
7 | import com.alexmercerind.audire.R
8 | import com.google.android.material.color.DynamicColors
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.withContext
13 |
14 | class SettingsRepository(private val application: Application) {
15 | val autoStart: StateFlow
16 | get() = _autoStart
17 | private val _autoStart = MutableStateFlow(false)
18 |
19 | val theme: StateFlow
20 | get() = _theme
21 | private val _theme = MutableStateFlow(null)
22 |
23 | val systemColorScheme: StateFlow
24 | get() = _systemColorScheme
25 | private val _systemColorScheme = MutableStateFlow(null)
26 |
27 | private val sharedPreferences =
28 | application.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
29 |
30 | private val autoStartSharedValue
31 | get() = sharedPreferences.getBoolean(
32 | SHARED_PREFERENCES_KEY_BEHAVIOR_AUTO_START,
33 | false
34 | )
35 | private val themeSharedValue
36 | get() = sharedPreferences.getString(
37 | SHARED_PREFERENCES_KEY_APPEARANCE_THEME,
38 | application.getString(R.string.settings_appearance_theme_system)
39 | ) ?: ""
40 | private val systemColorSchemeValue
41 | get() = sharedPreferences.getBoolean(
42 | SHARED_PREFERENCES_KEY_APPEARANCE_SYSTEM_COLOR_SCHEME,
43 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
44 | )
45 |
46 | init {
47 | _autoStart.value = autoStartSharedValue
48 | _theme.value = themeSharedValue
49 | _systemColorScheme.value = systemColorSchemeValue
50 | }
51 |
52 | suspend fun setAutoStart(value: Boolean) {
53 | sharedPreferences.edit().apply {
54 | putBoolean(SHARED_PREFERENCES_KEY_BEHAVIOR_AUTO_START, value)
55 | apply()
56 | }
57 | _autoStart.emit(value)
58 | }
59 |
60 | suspend fun setTheme(value: String) {
61 | sharedPreferences.edit().apply {
62 | putString(SHARED_PREFERENCES_KEY_APPEARANCE_THEME, value)
63 | apply()
64 | }
65 | _theme.emit(value)
66 | withContext(Dispatchers.Main) { applyTheme(value) }
67 | }
68 |
69 | suspend fun setSystemColorScheme(value: Boolean) {
70 | sharedPreferences.edit().apply {
71 | putBoolean(SHARED_PREFERENCES_KEY_APPEARANCE_SYSTEM_COLOR_SCHEME, value)
72 | apply()
73 | }
74 | _systemColorScheme.emit(value)
75 | withContext(Dispatchers.Main) { applySystemColorScheme(value) }
76 | }
77 |
78 | fun apply() {
79 | runCatching { applyTheme(themeSharedValue) }
80 | runCatching { applySystemColorScheme(systemColorSchemeValue) }
81 | }
82 |
83 | private fun applyTheme(value: String) {
84 | val mode = when (value) {
85 | application.getString(R.string.settings_appearance_theme_light) -> AppCompatDelegate.MODE_NIGHT_NO
86 | application.getString(R.string.settings_appearance_theme_dark) -> AppCompatDelegate.MODE_NIGHT_YES
87 | application.getString(R.string.settings_appearance_theme_system) -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
88 | else -> throw IllegalStateException()
89 | }
90 | AppCompatDelegate.setDefaultNightMode(mode)
91 | }
92 |
93 | private fun applySystemColorScheme(value: Boolean) {
94 | if (value) {
95 | DynamicColors.applyToActivitiesIfAvailable(application)
96 | } else {
97 | application.setTheme(R.style.Theme_Audire)
98 | }
99 | }
100 |
101 |
102 | companion object {
103 | private const val SHARED_PREFERENCES_NAME = "SETTINGS"
104 | private const val SHARED_PREFERENCES_KEY_BEHAVIOR_AUTO_START = "AUTO_START"
105 | private const val SHARED_PREFERENCES_KEY_APPEARANCE_THEME = "THEME"
106 | private const val SHARED_PREFERENCES_KEY_APPEARANCE_SYSTEM_COLOR_SCHEME = "SYSTEM_COLOR_SCHEME"
107 |
108 | @Volatile
109 | private var instance: SettingsRepository? = null
110 | private val lock = Any()
111 |
112 | operator fun invoke(application: Application) = instance ?: synchronized(lock) {
113 | instance ?: SettingsRepository(application).also { instance = it }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_history.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
32 |
33 |
34 |
46 |
47 |
56 |
57 |
58 |
65 |
66 |
73 |
74 |
75 |
82 |
83 |
88 |
89 |
90 |
91 |
98 |
99 |
100 |
107 |
108 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/MusicActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.app.SearchManager
4 | import android.content.ClipData
5 | import android.content.Intent
6 | import android.graphics.Bitmap
7 | import android.net.Uri
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.view.View
11 | import androidx.activity.enableEdgeToEdge
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.core.content.FileProvider
14 | import androidx.core.content.IntentCompat
15 | import androidx.core.graphics.drawable.toBitmap
16 | import androidx.core.view.ViewCompat
17 | import androidx.core.view.WindowInsetsCompat
18 | import androidx.core.view.updatePadding
19 | import androidx.lifecycle.lifecycleScope
20 | import coil.ImageLoader
21 | import coil.drawable.CrossfadeDrawable
22 | import coil.load
23 | import coil.request.CachePolicy
24 | import com.alexmercerind.audire.databinding.ActivityMusicBinding
25 | import com.alexmercerind.audire.mappers.toSearchQuery
26 | import com.alexmercerind.audire.mappers.toShareText
27 | import com.alexmercerind.audire.models.Music
28 | import kotlinx.coroutines.delay
29 | import kotlinx.coroutines.launch
30 | import java.io.File
31 | import java.io.FileOutputStream
32 |
33 | class MusicActivity : AppCompatActivity() {
34 | companion object {
35 | const val MUSIC = "MUSIC"
36 | }
37 |
38 | private lateinit var music: Music
39 | private lateinit var binding: ActivityMusicBinding
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | enableEdgeToEdge()
44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
45 | window.isNavigationBarContrastEnforced = false
46 | }
47 | binding = ActivityMusicBinding.inflate(layoutInflater)
48 | val view = binding.root
49 | setContentView(view)
50 |
51 | ViewCompat.setOnApplyWindowInsetsListener(binding.scrollView) { v, windowInsets ->
52 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
53 | v.updatePadding(top = insets.top)
54 | WindowInsetsCompat.CONSUMED
55 | }
56 | ViewCompat.setOnApplyWindowInsetsListener(binding.buttonGroup) { v, windowInsets ->
57 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
58 | v.updatePadding(
59 | top = 16,
60 | left = 16 + insets.left,
61 | right = 16 + insets.right,
62 | bottom = 16 + insets.bottom,
63 | )
64 | WindowInsetsCompat.CONSUMED
65 | }
66 |
67 | music = IntentCompat.getSerializableExtra(intent, MUSIC, Music::class.java)!!
68 |
69 | binding.titleTextView.text = music.title
70 | if (music.artists.isNotEmpty()) {
71 | binding.artistsTextView.text = music.artists
72 | } else {
73 | binding.artistsMaterialCardView.visibility = View.GONE
74 | }
75 | if (music.album != null) {
76 | binding.albumTextView.text = music.album
77 | } else {
78 | binding.albumMaterialCardView.visibility = View.GONE
79 | }
80 | if (music.label != null) {
81 | binding.labelTextView.text = music.label
82 | } else {
83 | binding.labelMaterialCardView.visibility = View.GONE
84 | }
85 | if (music.year != null) {
86 | binding.yearTextView.text = music.year
87 | } else {
88 | binding.yearMaterialCardView.visibility = View.GONE
89 | }
90 | binding.coverImageView.load(
91 | music.cover,
92 | ImageLoader.Builder(this)
93 | .memoryCachePolicy(CachePolicy.ENABLED)
94 | .diskCachePolicy(CachePolicy.ENABLED)
95 | .build()
96 | ) {
97 | crossfade(true)
98 | }
99 |
100 | lifecycleScope.launch {
101 | delay(2000L)
102 | binding.titleTextView.isSelected = true
103 | }
104 |
105 | binding.searchButton.setOnClickListener {
106 | startActivity(
107 | Intent(Intent.ACTION_WEB_SEARCH).apply {
108 | putExtra(SearchManager.QUERY, music.toSearchQuery())
109 | })
110 | }
111 | binding.shareButton.setOnClickListener {
112 | startActivity(
113 | Intent.createChooser(
114 | Intent().apply {
115 | action = Intent.ACTION_SEND
116 | type = "image/jpeg"
117 | flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
118 | clipData = ClipData.newRawUri(music.title, imageUri)
119 | putExtra(Intent.EXTRA_TEXT, music.toShareText())
120 | putExtra(Intent.EXTRA_STREAM, imageUri)
121 | },
122 | music.title
123 | )
124 | )
125 | }
126 | }
127 |
128 | private val imageUri: Uri? by lazy {
129 | return@lazy runCatching {
130 | val drawable = binding.coverImageView.drawable as CrossfadeDrawable
131 | val bitmap = drawable.end!!.toBitmap()
132 | val cache = File(cacheDir, "images")
133 | val image = File(cache, "cover.jpg")
134 | cache.mkdirs()
135 | FileOutputStream(image).use {
136 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
137 | }
138 | FileProvider.getUriForFile(
139 | this,
140 | "${applicationContext.packageName}.fileprovider",
141 | image
142 | )
143 | }.getOrElse {
144 | it.printStackTrace()
145 | null
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/HistoryFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.text.Editable
7 | import android.text.TextWatcher
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.view.inputmethod.InputMethodManager
12 | import androidx.fragment.app.Fragment
13 | import androidx.fragment.app.activityViewModels
14 | import androidx.lifecycle.Lifecycle
15 | import androidx.lifecycle.lifecycleScope
16 | import androidx.lifecycle.repeatOnLifecycle
17 | import androidx.recyclerview.widget.LinearLayoutManager
18 | import com.alexmercerind.audire.R
19 | import com.alexmercerind.audire.ui.adapters.HistoryItemAdapter
20 | import com.alexmercerind.audire.databinding.FragmentHistoryBinding
21 | import kotlinx.coroutines.flow.filterNotNull
22 | import kotlinx.coroutines.launch
23 |
24 | class HistoryFragment : Fragment() {
25 |
26 | private var _binding: FragmentHistoryBinding? = null
27 | private val binding get() = _binding!!
28 |
29 | private lateinit var imm: InputMethodManager
30 |
31 | private val historyViewModel: HistoryViewModel by activityViewModels()
32 |
33 | private val watcher = object : TextWatcher {
34 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
35 |
36 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
37 |
38 | override fun afterTextChanged(s: Editable?) {
39 | historyViewModel.query = s.toString()
40 | }
41 | }
42 |
43 | override fun onCreateView(
44 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
45 | ): View {
46 | imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
47 |
48 | _binding = FragmentHistoryBinding.inflate(inflater, container, false)
49 |
50 | binding.searchLinearLayout.visibility = View.GONE
51 | binding.historyLinearLayout.visibility = View.GONE
52 | binding.historyRecyclerView.adapter = HistoryItemAdapter(listOf(), historyViewModel)
53 | binding.historyRecyclerView.layoutManager = LinearLayoutManager(context)
54 |
55 | binding.searchTextInputLayout.visibility = View.GONE
56 |
57 | binding.searchTextInputLayout.setEndIconOnClickListener {
58 | binding.searchTextInputEditText.text?.clear()
59 | binding.searchTextInputLayout.visibility = View.GONE
60 | binding.searchTextInputLayout.clearFocus()
61 | imm.hideSoftInputFromWindow(binding.root.windowToken, 0)
62 | }
63 |
64 | viewLifecycleOwner.lifecycleScope.launch {
65 | repeatOnLifecycle(Lifecycle.State.STARTED) {
66 | historyViewModel.historyItems.filterNotNull().collect {
67 | if (it.isEmpty()) {
68 | binding.historyRecyclerView.visibility = View.GONE
69 | if (historyViewModel.query.isEmpty()) {
70 | // No HistoryItem(s) by default.
71 | binding.historyLinearLayout.visibility = View.VISIBLE
72 | binding.searchLinearLayout.visibility = View.GONE
73 | } else {
74 | // No HistoryItem(s) due to search.
75 | binding.historyLinearLayout.visibility = View.GONE
76 | binding.searchLinearLayout.visibility = View.VISIBLE
77 | }
78 | } else {
79 | // HistoryItem(s) are present i.e. RecyclerView must be VISIBLE.
80 | binding.historyRecyclerView.visibility = View.VISIBLE
81 | binding.historyLinearLayout.visibility = View.GONE
82 | binding.searchLinearLayout.visibility = View.GONE
83 |
84 | val adapter = binding.historyRecyclerView.adapter as HistoryItemAdapter
85 | if (adapter.items.size != it.size) {
86 | adapter.items = it
87 | adapter.notifyDataSetChanged()
88 | } else {
89 | adapter.items = it
90 | adapter.notifyItemRangeChanged(0, it.size)
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
97 |
98 | binding.primaryMaterialToolbar.setOnMenuItemClickListener {
99 | if (it.itemId == R.id.search) {
100 | binding.searchTextInputLayout.visibility = View.VISIBLE
101 | binding.searchTextInputLayout.requestFocus()
102 | imm.showSoftInput(binding.searchTextInputEditText, 0)
103 | } else {
104 | val intent = when (it.itemId) {
105 | R.id.settings -> Intent(context, SettingsActivity::class.java)
106 | R.id.about -> Intent(context, AboutActivity::class.java)
107 | else -> null
108 | }
109 | if (intent != null) {
110 | startActivity(intent)
111 | }
112 | }
113 | true
114 | }
115 |
116 | return binding.root
117 | }
118 |
119 | override fun onStart() {
120 | super.onStart()
121 | binding.searchTextInputEditText.addTextChangedListener(watcher)
122 | }
123 |
124 | override fun onStop() {
125 | super.onStop()
126 | binding.searchTextInputEditText.removeTextChangedListener(watcher)
127 | binding.searchTextInputEditText.text?.clear()
128 | historyViewModel.query = ""
129 | }
130 |
131 | override fun onDestroyView() {
132 | super.onDestroyView()
133 | _binding = null
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/LikedFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.text.Editable
7 | import android.text.TextWatcher
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.view.inputmethod.InputMethodManager
12 | import androidx.fragment.app.Fragment
13 | import androidx.fragment.app.viewModels
14 | import androidx.lifecycle.Lifecycle
15 | import androidx.lifecycle.lifecycleScope
16 | import androidx.lifecycle.repeatOnLifecycle
17 | import androidx.recyclerview.widget.LinearLayoutManager
18 | import com.alexmercerind.audire.R
19 | import com.alexmercerind.audire.ui.adapters.HistoryItemAdapter
20 | import com.alexmercerind.audire.databinding.FragmentHistoryBinding
21 | import kotlinx.coroutines.flow.filterNotNull
22 | import kotlinx.coroutines.launch
23 |
24 | class LikedFragment : Fragment() {
25 |
26 | private var _binding: FragmentHistoryBinding? = null
27 | private val binding get() = _binding!!
28 |
29 | private lateinit var imm: InputMethodManager
30 |
31 | private val historyViewModel: HistoryViewModel by viewModels()
32 |
33 | private val watcher = object : TextWatcher {
34 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
35 |
36 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
37 |
38 | override fun afterTextChanged(s: Editable?) {
39 | historyViewModel.query = s.toString()
40 | }
41 | }
42 |
43 | override fun onCreateView(
44 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
45 | ): View {
46 | imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
47 |
48 | _binding = FragmentHistoryBinding.inflate(inflater, container, false)
49 |
50 | binding.searchLinearLayout.visibility = View.GONE
51 | binding.historyLinearLayout.visibility = View.GONE
52 | binding.historyRecyclerView.adapter = HistoryItemAdapter(listOf(), historyViewModel)
53 | binding.historyRecyclerView.layoutManager = LinearLayoutManager(context)
54 |
55 | binding.searchTextInputLayout.visibility = View.GONE
56 |
57 | binding.searchTextInputLayout.setEndIconOnClickListener {
58 | binding.searchTextInputEditText.text?.clear()
59 | binding.primaryMaterialToolbar.visibility = View.VISIBLE
60 | binding.searchTextInputLayout.visibility = View.GONE
61 | binding.searchTextInputLayout.clearFocus()
62 | imm.hideSoftInputFromWindow(binding.root.windowToken, 0)
63 | }
64 |
65 | viewLifecycleOwner.lifecycleScope.launch {
66 | repeatOnLifecycle(Lifecycle.State.STARTED) {
67 | historyViewModel.historyItems.filterNotNull().collect { it ->
68 | it.filter { it.liked }.let {
69 | if (it.isEmpty()) {
70 | binding.historyRecyclerView.visibility = View.GONE
71 | if (historyViewModel.query.isEmpty()) {
72 | // No HistoryItem(s) by default.
73 | binding.historyLinearLayout.visibility = View.VISIBLE
74 | binding.searchLinearLayout.visibility = View.GONE
75 | } else {
76 | // No HistoryItem(s) due to search.
77 | binding.historyLinearLayout.visibility = View.GONE
78 | binding.searchLinearLayout.visibility = View.VISIBLE
79 | }
80 | } else {
81 | // HistoryItem(s) are present i.e. RecyclerView must be VISIBLE.
82 | binding.historyRecyclerView.visibility = View.VISIBLE
83 | binding.historyLinearLayout.visibility = View.GONE
84 | binding.searchLinearLayout.visibility = View.GONE
85 | val adapter = binding.historyRecyclerView.adapter as HistoryItemAdapter
86 | if (adapter.items.size != it.size) {
87 | adapter.items = it
88 | adapter.notifyDataSetChanged()
89 | } else {
90 | adapter.items = it
91 | adapter.notifyItemRangeChanged(0, it.size)
92 | }
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 |
100 | binding.primaryMaterialToolbar.setOnMenuItemClickListener {
101 | if (it.itemId == R.id.search) {
102 | binding.primaryMaterialToolbar.visibility = View.GONE
103 | binding.searchTextInputLayout.visibility = View.VISIBLE
104 | binding.searchTextInputLayout.requestFocus()
105 | imm.showSoftInput(binding.searchTextInputEditText, 0)
106 | } else {
107 | val intent = when (it.itemId) {
108 | R.id.settings -> Intent(context, SettingsActivity::class.java)
109 | R.id.about -> Intent(context, AboutActivity::class.java)
110 | else -> null
111 | }
112 | if (intent != null) {
113 | startActivity(intent)
114 | }
115 | }
116 | true
117 | }
118 |
119 | return binding.root
120 | }
121 |
122 | override fun onStart() {
123 | super.onStart()
124 | binding.searchTextInputEditText.addTextChangedListener(watcher)
125 | }
126 |
127 | override fun onStop() {
128 | super.onStop()
129 | binding.searchTextInputEditText.removeTextChangedListener(watcher)
130 | binding.searchTextInputEditText.text?.clear()
131 |
132 | historyViewModel.query = ""
133 | }
134 |
135 | override fun onDestroyView() {
136 | super.onDestroyView()
137 | _binding = null
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.Gravity
6 | import android.widget.PopupMenu
7 | import androidx.activity.enableEdgeToEdge
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.activity.viewModels
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.lifecycle.Lifecycle
12 | import androidx.lifecycle.lifecycleScope
13 | import androidx.lifecycle.repeatOnLifecycle
14 | import com.alexmercerind.audire.R
15 | import com.alexmercerind.audire.databinding.ActivitySettingsBinding
16 | import com.google.android.material.snackbar.Snackbar
17 | import kotlinx.coroutines.flow.distinctUntilChanged
18 | import kotlinx.coroutines.flow.filterNotNull
19 | import kotlinx.coroutines.launch
20 |
21 | class SettingsActivity : AppCompatActivity() {
22 | private val settingsViewModel: SettingsViewModel by viewModels()
23 |
24 | private lateinit var binding: ActivitySettingsBinding
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | enableEdgeToEdge()
28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
29 | window.isNavigationBarContrastEnforced = false
30 | }
31 | binding = ActivitySettingsBinding.inflate(layoutInflater)
32 | setContentView(binding.root)
33 |
34 | val exportLauncher =
35 | registerForActivityResult(ActivityResultContracts.CreateDocument(getString(R.string.settings_backup_file_mime))) { uri ->
36 | if (uri != null) {
37 | lifecycleScope.launch {
38 | try {
39 | settingsViewModel.export(uri)
40 | Snackbar.make(binding.root, R.string.settings_backup_export_success, Snackbar.LENGTH_LONG).show()
41 | } catch (e: Throwable) {
42 | e.printStackTrace()
43 | Snackbar.make(binding.root, R.string.settings_backup_export_fail, Snackbar.LENGTH_LONG).show()
44 | }
45 | }
46 | }
47 | }
48 | val importLauncher =
49 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
50 | if (uri != null) {
51 | lifecycleScope.launch {
52 | try {
53 | settingsViewModel.import(uri)
54 | Snackbar.make(binding.root, R.string.settings_backup_import_success, Snackbar.LENGTH_LONG).show()
55 | } catch (e: Throwable) {
56 | e.printStackTrace()
57 | Snackbar.make(binding.root, R.string.settings_backup_import_fail, Snackbar.LENGTH_LONG).show()
58 | }
59 | }
60 | }
61 | }
62 |
63 | lifecycleScope.launch {
64 | repeatOnLifecycle(Lifecycle.State.STARTED) {
65 | settingsViewModel.autoStart.filterNotNull().distinctUntilChanged().collect {
66 | if (binding.settingsBehaviorAutoStartMaterialSwitch.isChecked != it) {
67 | binding.settingsBehaviorAutoStartMaterialSwitch.isChecked = it
68 | }
69 | }
70 | }
71 | }
72 |
73 | lifecycleScope.launch {
74 | repeatOnLifecycle(Lifecycle.State.STARTED) {
75 | settingsViewModel.theme.filterNotNull().distinctUntilChanged().collect {
76 | binding.settingsAppearanceThemeSupportingText.text = it
77 | }
78 | }
79 | }
80 | lifecycleScope.launch {
81 | repeatOnLifecycle(Lifecycle.State.STARTED) {
82 | settingsViewModel.systemColorScheme.filterNotNull().distinctUntilChanged().collect {
83 | if (binding.settingsAppearanceSystemColorSchemeMaterialSwitch.isChecked != it) {
84 | binding.settingsAppearanceSystemColorSchemeMaterialSwitch.isChecked = it
85 | }
86 | }
87 | }
88 | }
89 |
90 | binding.materialToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
91 |
92 | binding.settingsBehaviorAutoStartLinearLayout.setOnClickListener {
93 | settingsViewModel.setAutoStart(!binding.settingsBehaviorAutoStartMaterialSwitch.isChecked)
94 | }
95 | binding.settingsBehaviorAutoStartMaterialSwitch.setOnCheckedChangeListener { view, checked ->
96 | settingsViewModel.setAutoStart(checked)
97 | }
98 |
99 | binding.settingsAppearanceThemeLinearLayout.setOnClickListener {
100 | val popup = PopupMenu(
101 | this,
102 | binding.settingsAppearanceThemePopupMenuAnchor,
103 | Gravity.CENTER,
104 | )
105 | popup.apply {
106 | setOnMenuItemClickListener { item ->
107 | settingsViewModel.setTheme(item.toString())
108 | true
109 | }
110 | menu.add(R.string.settings_appearance_theme_light)
111 | menu.add(R.string.settings_appearance_theme_dark)
112 | menu.add(R.string.settings_appearance_theme_system)
113 | show()
114 | }
115 |
116 | }
117 |
118 | binding.settingsAppearanceSystemColorSchemeLinearLayout.setOnClickListener {
119 | settingsViewModel.setSystemColorScheme(!binding.settingsAppearanceSystemColorSchemeMaterialSwitch.isChecked)
120 |
121 | Snackbar.make(binding.root, R.string.settings_application_restart_required, Snackbar.LENGTH_LONG).apply {
122 | setAction(R.string.ok) { dismiss() }
123 | show()
124 | }
125 |
126 | }
127 | binding.settingsAppearanceSystemColorSchemeMaterialSwitch.setOnCheckedChangeListener { view, checked ->
128 | settingsViewModel.setSystemColorScheme(checked)
129 |
130 | if (view.isPressed) {
131 | Snackbar.make(binding.root, R.string.settings_application_restart_required, Snackbar.LENGTH_LONG).apply {
132 | setAction(R.string.ok) { dismiss() }
133 | show()
134 | }
135 | }
136 | }
137 |
138 | binding.settingsBackupExportLinearLayout.setOnClickListener {
139 | exportLauncher.launch(getString(R.string.settings_backup_file_name))
140 | }
141 | binding.settingsBackupImportLinearLayout.setOnClickListener {
142 | importLauncher.launch(arrayOf(getString(R.string.settings_backup_file_mime)))
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/ui/IdentifyFragment.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.ui
2 |
3 | import android.Manifest
4 | import android.animation.ObjectAnimator
5 | import android.animation.PropertyValuesHolder
6 | import android.content.Intent
7 | import android.content.pm.PackageManager
8 | import android.os.Bundle
9 | import android.text.format.DateUtils
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.view.animation.AccelerateDecelerateInterpolator
14 | import androidx.activity.result.contract.ActivityResultContracts
15 | import androidx.core.app.ActivityCompat
16 | import androidx.fragment.app.Fragment
17 | import androidx.fragment.app.activityViewModels
18 | import androidx.lifecycle.Lifecycle
19 | import androidx.lifecycle.lifecycleScope
20 | import androidx.lifecycle.repeatOnLifecycle
21 | import com.alexmercerind.audire.R
22 | import com.alexmercerind.audire.databinding.FragmentIdentifyBinding
23 | import com.alexmercerind.audire.mappers.toHistoryItem
24 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
25 | import com.google.android.material.snackbar.Snackbar
26 | import kotlinx.coroutines.launch
27 |
28 | class IdentifyFragment : Fragment() {
29 | private var _binding: FragmentIdentifyBinding? = null
30 | private val binding get() = _binding!!
31 |
32 | private val identifyViewModel: IdentifyViewModel by activityViewModels()
33 | private val historyViewModel: HistoryViewModel by activityViewModels()
34 |
35 | private lateinit var idleFloatingActionButtonObjectAnimator: ObjectAnimator
36 | private lateinit var visibilityRecordFloatingActionButtonObjectAnimator: ObjectAnimator
37 | private lateinit var visibilityStopButtonObjectAnimator: ObjectAnimator
38 | private lateinit var visibilityWaveViewObjectAnimator: ObjectAnimator
39 |
40 | override fun onCreateView(
41 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
42 | ): View {
43 | _binding = FragmentIdentifyBinding.inflate(inflater, container, false)
44 | val view = binding.root
45 | val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
46 | if (it) {
47 | // FUCK YOU
48 | // identifyViewModel.start()
49 | } else {
50 | showRecordAudioPermissionNotAvailableDialog()
51 | }
52 | }
53 | binding.recordFloatingActionButton.setOnClickListener {
54 | if (ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
55 | identifyViewModel.start()
56 | } else {
57 | runCatching { launcher.launch(Manifest.permission.RECORD_AUDIO) }
58 | }
59 | }
60 | binding.stopButton.setOnClickListener {
61 | identifyViewModel.stop()
62 | }
63 |
64 | viewLifecycleOwner.lifecycleScope.launch {
65 | repeatOnLifecycle(Lifecycle.State.STARTED) {
66 | launch {
67 | identifyViewModel.error.collect {
68 | Snackbar.make(view, R.string.identify_error, Snackbar.LENGTH_LONG).apply {
69 | anchorView = requireActivity().findViewById(R.id.bottomNavigationView)
70 | show()
71 | }
72 | }
73 | }
74 | launch {
75 | identifyViewModel.music.collect {
76 | if (isVisible) {
77 | // Show the MusicActivity.
78 | val intent = Intent(context, MusicActivity::class.java).apply {
79 | putExtra(MusicActivity.MUSIC, it)
80 | }
81 | startActivity(intent)
82 | // Add to Room database.
83 | runCatching { historyViewModel.insert(it.toHistoryItem()) }
84 | }
85 | }
86 | }
87 | launch {
88 | identifyViewModel.active.collect {
89 | when (it) {
90 | false -> animateToRecordButton()
91 | true -> animateToStopButton()
92 | }
93 | }
94 | }
95 | launch {
96 | identifyViewModel.duration.collect {
97 | binding.stopButton.text = DateUtils.formatElapsedTime(it.toLong())
98 | }
99 | }
100 | }
101 | }
102 |
103 | idleFloatingActionButtonObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(
104 | binding.recordFloatingActionButton,
105 | PropertyValuesHolder.ofFloat(View.SCALE_X, 1.0F, 1.4F),
106 | PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.0F, 1.4F),
107 | ).apply {
108 | duration = 2000L
109 | interpolator = AccelerateDecelerateInterpolator()
110 | repeatCount = ObjectAnimator.INFINITE
111 | repeatMode = ObjectAnimator.REVERSE
112 | }
113 | visibilityRecordFloatingActionButtonObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(
114 | binding.recordFloatingActionButton,
115 | PropertyValuesHolder.ofFloat(View.SCALE_X, 0.5F, 1.0F),
116 | PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.5F, 1.0F),
117 | PropertyValuesHolder.ofFloat(View.ALPHA, 0.0F, 1.0F),
118 | ).apply {
119 | duration = 200L
120 | interpolator = AccelerateDecelerateInterpolator()
121 | }
122 | visibilityStopButtonObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(
123 | binding.stopButton,
124 | PropertyValuesHolder.ofFloat(View.SCALE_X, 0.5F, 1.0F),
125 | PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.5F, 1.0F),
126 | PropertyValuesHolder.ofFloat(View.ALPHA, 0.0F, 1.0F),
127 | ).apply {
128 | duration = 200L
129 | interpolator = AccelerateDecelerateInterpolator()
130 | }
131 | visibilityWaveViewObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(
132 | binding.waveView,
133 | PropertyValuesHolder.ofFloat(View.ALPHA, 0.0F, 1.0F),
134 | ).apply {
135 | duration = 500L
136 | interpolator = AccelerateDecelerateInterpolator()
137 | }
138 |
139 | if (identifyViewModel.active.value) {
140 | binding.recordFloatingActionButton.scaleX = 0.5F
141 | binding.recordFloatingActionButton.scaleY = 0.5F
142 | binding.recordFloatingActionButton.alpha = 0.0F
143 | binding.stopButton.scaleX = 1.0F
144 | binding.stopButton.scaleY = 1.0F
145 | binding.stopButton.alpha = 1.0F
146 | binding.waveView.alpha = 1.0F
147 | idleFloatingActionButtonObjectAnimator.cancel()
148 | } else {
149 | binding.recordFloatingActionButton.scaleX = 1.0F
150 | binding.recordFloatingActionButton.scaleY = 1.0F
151 | binding.recordFloatingActionButton.alpha = 1.0F
152 | binding.stopButton.scaleX = 0.5F
153 | binding.stopButton.scaleY = 0.5F
154 | binding.stopButton.alpha = 0.0F
155 | binding.waveView.alpha = 0.0F
156 | idleFloatingActionButtonObjectAnimator.start()
157 | }
158 |
159 | binding.primaryMaterialToolbar.setOnMenuItemClickListener {
160 | val intent = when (it.itemId) {
161 | R.id.settings -> Intent(context, SettingsActivity::class.java)
162 | R.id.about -> Intent(context, AboutActivity::class.java)
163 | else -> null
164 | }
165 | if (intent != null) {
166 | startActivity(intent)
167 | }
168 | true
169 | }
170 |
171 | return view
172 | }
173 |
174 | private fun animateToRecordButton() {
175 | idleFloatingActionButtonObjectAnimator.start()
176 |
177 | if (binding.recordFloatingActionButton.alpha == 0.0F) {
178 | visibilityRecordFloatingActionButtonObjectAnimator.start()
179 | }
180 | if (binding.stopButton.alpha == 1.0F) {
181 | visibilityStopButtonObjectAnimator.reverse()
182 | }
183 | if (binding.waveView.alpha == 1.0F) {
184 | visibilityWaveViewObjectAnimator.reverse()
185 | }
186 | }
187 |
188 | private fun animateToStopButton() {
189 | idleFloatingActionButtonObjectAnimator.cancel()
190 |
191 | if (binding.recordFloatingActionButton.alpha == 1.0F) {
192 | visibilityRecordFloatingActionButtonObjectAnimator.reverse()
193 | }
194 | if (binding.stopButton.alpha == 0.0F) {
195 | visibilityStopButtonObjectAnimator.start()
196 | }
197 | if (binding.waveView.alpha == 0.0F) {
198 | visibilityWaveViewObjectAnimator.start()
199 | }
200 | }
201 |
202 | private fun showRecordAudioPermissionNotAvailableDialog() {
203 | MaterialAlertDialogBuilder(
204 | requireActivity(), R.style.Base_Theme_Audire_MaterialAlertDialog
205 | ).setTitle(R.string.identify_record_permission_alert_dialog_not_available_title)
206 | .setMessage(R.string.identify_record_permission_alert_dialog_not_available_message)
207 | .setPositiveButton(R.string.ok) { dialog, _ -> dialog?.dismiss() }.create().show()
208 |
209 | }
210 |
211 | override fun onDestroyView() {
212 | super.onDestroyView()
213 | _binding = null
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alexmercerind/audire/repository/ShazamIdentifyRepository.kt:
--------------------------------------------------------------------------------
1 | package com.alexmercerind.audire.repository
2 |
3 | import com.alexmercerind.audire.api.shazam.ShazamAPI
4 | import com.alexmercerind.audire.api.shazam.models.Geolocation
5 | import com.alexmercerind.audire.api.shazam.models.ShazamRequestBody
6 | import com.alexmercerind.audire.api.shazam.models.Signature
7 | import com.alexmercerind.audire.mappers.toMusic
8 | import com.alexmercerind.audire.mappers.toShortArray
9 | import com.alexmercerind.audire.models.Music
10 | import com.alexmercerind.audire.native.ShazamSignature
11 | import com.github.f4b6a3.uuid.UuidCreator
12 | import com.github.f4b6a3.uuid.enums.UuidNamespace
13 | import okhttp3.OkHttpClient
14 | import okhttp3.logging.HttpLoggingInterceptor
15 | import retrofit2.Retrofit
16 | import retrofit2.converter.gson.GsonConverterFactory
17 | import java.util.Calendar
18 | import kotlin.random.Random
19 |
20 | class ShazamIdentifyRepository : IdentifyRepository() {
21 | private val client: OkHttpClient by lazy {
22 | OkHttpClient.Builder()
23 | .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
24 | .build()
25 | }
26 |
27 | private val api: ShazamAPI by lazy {
28 | Retrofit.Builder()
29 | .client(client)
30 | .baseUrl(BASE_URL)
31 | .addConverterFactory(GsonConverterFactory.create())
32 | .build()
33 | .create(ShazamAPI::class.java)
34 | }
35 |
36 | override suspend fun identify(duration: Int, data: ByteArray): Music? {
37 | val timestamp = Calendar.getInstance().time.time.toInt()
38 | val name = Random(timestamp).nextInt(1 shl 48).toString()
39 | val body = ShazamRequestBody(
40 | Geolocation(
41 | Random(timestamp).nextDouble() * 400 + 100,
42 | Random(timestamp).nextDouble() * 180 - 90,
43 | Random(timestamp).nextDouble() * 360 - 180
44 | ),
45 | Signature(
46 | duration * 1000,
47 | timestamp,
48 | ShazamSignature().create(data.toShortArray())
49 | ),
50 | timestamp,
51 | TIMEZONES.random()
52 | )
53 | val response = api.discovery(
54 | body,
55 | UuidCreator.getNameBasedSha1(UuidNamespace.NAMESPACE_DNS, name).toString(),
56 | UuidCreator.getNameBasedSha1(UuidNamespace.NAMESPACE_URL, name).toString(),
57 | USER_AGENTS.random(),
58 | )
59 | return response.body()?.toMusic()
60 | }
61 |
62 | companion object {
63 | private const val BASE_URL = "https://amp.shazam.com/"
64 | private val USER_AGENTS = arrayOf(
65 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; VS980 4G Build/LRX22G)",
66 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-T210 Build/KOT49H)",
67 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-P905V Build/LMY47X)",
68 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; Vodafone Smart Tab 4G Build/KTU84P)",
69 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-G360H Build/KTU84P)",
70 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; SM-S920L Build/LRX22G)",
71 | "Dalvik/2.1.0 (Linux; U; Android 5.0; Fire Pro Build/LRX21M)",
72 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
73 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G920F Build/MMB29K)",
74 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G7102 Build/KOT49H)",
75 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
76 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G928F Build/MMB29K)",
77 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J500FN Build/LMY48B)",
78 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; Coolpad 3320A Build/LMY47V)",
79 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-J110F Build/KTU84P)",
80 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SAMSUNG-SGH-I747 Build/KOT49H)",
81 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SAMSUNG-SM-T337A Build/KOT49H)",
82 | "Dalvik/1.6.0 (Linux; U; Android 4.3; SGH-T999 Build/JSS15J)",
83 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; D6603 Build/23.5.A.0.570)",
84 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J700H Build/LMY48B)",
85 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; HTC6600LVW Build/KOT49H)",
86 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910G Build/LMY47X)",
87 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910T Build/LMY47X)",
88 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; C6903 Build/14.4.A.0.157)",
89 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G920F Build/MMB29K)",
90 | "Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-I9105P Build/JDQ39)",
91 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
92 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9192 Build/KOT49H)",
93 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G531H Build/LMY48B)",
94 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
95 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; LGMS345 Build/LMY47V)",
96 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; HTC One Build/LRX22G)",
97 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; LG-D800 Build/LRX22G)",
98 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G531H Build/LMY48B)",
99 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
100 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-T113 Build/KTU84P)",
101 | "Dalvik/1.6.0 (Linux; U; Android 4.2.2; AndyWin Build/JDQ39E)",
102 | "Dalvik/2.1.0 (Linux; U; Android 5.0; Lenovo A7000-a Build/LRX21M)",
103 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; LGL16C Build/KOT49I.L16CV11a)",
104 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
105 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; SM-A700FD Build/LRX22G)",
106 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G130HN Build/KOT49H)",
107 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-N9005 Build/KOT49H)",
108 | "Dalvik/1.6.0 (Linux; U; Android 4.1.2; LG-E975T Build/JZO54K)",
109 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; E1 Build/KOT49H)",
110 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
111 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-N5100 Build/KOT49H)",
112 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-A310F Build/LMY47X)",
113 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J105H Build/LMY47V)",
114 | "Dalvik/1.6.0 (Linux; U; Android 4.3; GT-I9305T Build/JSS15J)",
115 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; android Build/JDQ39)",
116 | "Dalvik/1.6.0 (Linux; U; Android 4.2.1; HS-U970 Build/JOP40D)",
117 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-T561 Build/KTU84P)",
118 | "Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-P3110 Build/JDQ39)",
119 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G925T Build/MMB29K)",
120 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; HUAWEI Y221-U22 Build/HUAWEIY221-U22)",
121 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G530T1 Build/LMY47X)",
122 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G920I Build/LMY47X)",
123 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
124 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; Vodafone Smart ultra 6 Build/LMY47V)",
125 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; XT1080 Build/SU6-7.7)",
126 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; ASUS MeMO Pad 7 Build/KTU84P)",
127 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G800F Build/KOT49H)",
128 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-N7100 Build/KOT49H)",
129 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G925I Build/MMB29K)",
130 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; A0001 Build/MMB29X)",
131 | "Dalvik/2.1.0 (Linux; U; Android 5.1; XT1045 Build/LPB23.13-61)",
132 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; LGMS330 Build/LMY47V)",
133 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; Z970 Build/KTU84P)",
134 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N900P Build/LRX21V)",
135 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; T1-701u Build/HuaweiMediaPad)",
136 | "Dalvik/2.1.0 (Linux; U; Android 5.1; HTCD100LVWPP Build/LMY47O)",
137 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G935R4 Build/MMB29M)",
138 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G930V Build/MMB29M)",
139 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; ZTE Blade Q Lux Build/LRX22G)",
140 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; GT-I9060I Build/KTU84P)",
141 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; LGUS992 Build/MMB29M)",
142 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900P Build/MMB29M)",
143 | "Dalvik/1.6.0 (Linux; U; Android 4.1.2; SGH-T999L Build/JZO54K)",
144 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910V Build/LMY47X)",
145 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
146 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-P601 Build/LMY47X)",
147 | "Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-S7272 Build/JDQ39)",
148 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910T Build/LMY47X)",
149 | "Dalvik/1.6.0 (Linux; U; Android 4.3; SAMSUNG-SGH-I747 Build/JSS15J)",
150 | "Dalvik/2.1.0 (Linux; U; Android 5.0.2; ZTE Blade Q Lux Build/LRX22G)",
151 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G930F Build/MMB29K)",
152 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; HTC_PO582 Build/KOT49H)",
153 | "Dalvik/2.1.0 (Linux; U; Android 6.0; HUAWEI MT7-TL10 Build/HuaweiMT7-TL10)",
154 | "Dalvik/2.1.0 (Linux; U; Android 6.0; LG-H811 Build/MRA58K)",
155 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-N7505 Build/KOT49H)",
156 | "Dalvik/2.1.0 (Linux; U; Android 6.0; LG-H815 Build/MRA58K)",
157 | "Dalvik/1.6.0 (Linux; U; Android 4.4.2; LenovoA3300-HV Build/KOT49H)",
158 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-G360G Build/KTU84P)",
159 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; GT-I9300I Build/KTU84P)",
160 | "Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
161 | "Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-J700T Build/MMB29K)",
162 | "Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J500FN Build/LMY48B)",
163 | "Dalvik/1.6.0 (Linux; U; Android 4.2.2; SM-T217S Build/JDQ39)",
164 | "Dalvik/1.6.0 (Linux; U; Android 4.4.4; SAMSUNG-SM-N900A Build/KTU84P)"
165 | )
166 | private val TIMEZONES = arrayOf(
167 | "Europe/Amsterdam",
168 | "Europe/Andorra",
169 | "Europe/Astrakhan",
170 | "Europe/Athens",
171 | "Europe/Belgrade",
172 | "Europe/Berlin",
173 | "Europe/Bratislava",
174 | "Europe/Brussels",
175 | "Europe/Bucharest",
176 | "Europe/Budapest",
177 | "Europe/Busingen",
178 | "Europe/Chisinau",
179 | "Europe/Copenhagen",
180 | "Europe/Dublin",
181 | "Europe/Gibraltar",
182 | "Europe/Guernsey",
183 | "Europe/Helsinki",
184 | "Europe/Isle_of_Man",
185 | "Europe/Istanbul",
186 | "Europe/Jersey",
187 | "Europe/Kaliningrad",
188 | "Europe/Kirov",
189 | "Europe/Kyiv",
190 | "Europe/Lisbon",
191 | "Europe/Ljubljana",
192 | "Europe/London",
193 | "Europe/Luxembourg",
194 | "Europe/Madrid",
195 | "Europe/Malta",
196 | "Europe/Mariehamn",
197 | "Europe/Minsk",
198 | "Europe/Monaco",
199 | "Europe/Moscow",
200 | "Europe/Oslo",
201 | "Europe/Paris",
202 | "Europe/Podgorica",
203 | "Europe/Prague",
204 | "Europe/Riga",
205 | "Europe/Rome",
206 | "Europe/Samara",
207 | "Europe/San_Marino",
208 | "Europe/Sarajevo",
209 | "Europe/Saratov",
210 | "Europe/Simferopol",
211 | "Europe/Skopje",
212 | "Europe/Sofia",
213 | "Europe/Stockholm",
214 | "Europe/Tallinn",
215 | "Europe/Tirane",
216 | "Europe/Ulyanovsk",
217 | "Europe/Vaduz",
218 | "Europe/Vatican",
219 | "Europe/Vienna",
220 | "Europe/Vilnius",
221 | "Europe/Volgograd",
222 | "Europe/Warsaw",
223 | "Europe/Zagreb",
224 | "Europe/Zurich"
225 | )
226 | }
227 | }
228 |
--------------------------------------------------------------------------------