diff --git a/.github/assets/add-light.png b/.github/assets/add-light.png
new file mode 100644
index 0000000..e2f0938
Binary files /dev/null and b/.github/assets/add-light.png differ
diff --git a/.github/assets/filter-light.png b/.github/assets/filter-light.png
new file mode 100644
index 0000000..b59d3a7
Binary files /dev/null and b/.github/assets/filter-light.png differ
diff --git a/.github/assets/logo.png b/.github/assets/logo.png
new file mode 100644
index 0000000..14f84b1
Binary files /dev/null and b/.github/assets/logo.png differ
diff --git a/.github/assets/main-display-dark.png b/.github/assets/main-display-dark.png
new file mode 100644
index 0000000..4a904f4
Binary files /dev/null and b/.github/assets/main-display-dark.png differ
diff --git a/.github/assets/main-display-light.png b/.github/assets/main-display-light.png
new file mode 100644
index 0000000..3bb16ea
Binary files /dev/null and b/.github/assets/main-display-light.png differ
diff --git a/.github/assets/search-light.png b/.github/assets/search-light.png
new file mode 100644
index 0000000..5b73aed
Binary files /dev/null and b/.github/assets/search-light.png differ
diff --git a/.gitignore b/.gitignore
index 39b6783..bd39932 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@ captures/
.idea/dictionaries
.idea/libraries
.idea/caches
+.idea/sonarlint
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
@@ -63,3 +64,16 @@ fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
+
+# Default gitignore from Android Studio
+*.iml
+.gradle
+/local.properties
+/.idea/caches/build_file_checksums.ser
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..30aa626
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..77523ec
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..a6b0c90
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..8725429
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Marc Donald
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index e31755f..780a1c8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,73 @@
+
+
# Earworm
-An app for tracking your favourite songs throughout time
+An app for tracking your favourite music throughout time.
+
+## Download
+[Click Here to Go To the Latest Release](https://github.com/MarcDonald/Earworm/releases/latest)
+
+## Features
+- Add the names of songs/albums/artists that you have been listening to recently
+- Add artwork
+- Search by artist name/album name/song name/genre
+- Filter by date or type
+- Dark theme
+
+## Screenshots
+| Main Screen | Add Screen | Main Screen Dark |
+|:-:|:-:|:-:|
+| ![Main Screen](/.github/assets/main-display-light.png?raw=true) | ![Add Screen](/.github/assets/add-light.png?raw=true) |![Dark Theme](/.github/assets/main-display-dark.png?raw=true)
+
+| Search | Filter |
+|:-:|:-:|
+| ![Search](/.github/assets/search-light.png?raw=true) | ![Filter](/.github/assets/filter-light.png?raw=true) |
+
+## Open Source Libraries Used
+### [Timber](https://github.com/JakeWharton/timber)
+Used for logging
+
+Apache 2 License
+
+### [Glide](https://github.com/bumptech/glide)
+Used for image loading and caching
+
+[License](https://github.com/bumptech/glide/blob/master/LICENSE)
+
+### [Material Design Icons](https://github.com/google/material-design-icons)
+Used for all the icons within the app
+
+Apache 2 License
+
+### [Android File Picker](https://github.com/DroidNinja/Android-FilePicker)
+Used for selecting an image
+
+Apache 2 License
+
+### [Material Components for Android](https://github.com/material-components/material-components-android)
+Used for implementing material design components
+
+Apache 2 License
+
+## License
+```
+The MIT License (MIT)
+
+Copyright (c) 2018 Marc Donald
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..109feca
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+apply plugin: 'kotlin-kapt'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "app.marcdev.earworm"
+ minSdkVersion 23
+ targetSdkVersion 28
+ versionCode 12
+ versionName "1.0.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.preference:preference:1.0.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
+ implementation 'com.google.android.material:material:1.0.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.1.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
+
+ // Timber for logging
+ implementation('com.jakewharton.timber:timber:4.7.0')
+
+ // Room components
+ implementation 'androidx.room:room-runtime:2.0.0'
+ kapt 'androidx.room:room-compiler:2.0.0'
+ androidTestImplementation 'androidx.room:room-testing:2.0.0'
+
+ // Kotlin co-routines for asynchronous code
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
+
+ // Glide for image loading and caching
+ implementation 'com.github.bumptech.glide:glide:4.8.0'
+ kapt 'com.github.bumptech.glide:compiler:4.8.0'
+
+ // Android File Picker for choosing an image
+ implementation 'com.droidninja:filepicker:2.2.0'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt b/app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt
new file mode 100644
index 0000000..f7311c2
--- /dev/null
+++ b/app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt
@@ -0,0 +1,246 @@
+package app.marcdev.earworm.database
+
+import androidx.room.Room
+import androidx.test.platform.app.InstrumentationRegistry
+import app.marcdev.earworm.utils.SONG
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+
+class DAOTest {
+
+ private var database: AppDatabase? = null
+ private var dao: DAO? = null
+
+ // Default values
+ private val testName = "Test Song Name"
+ private val testAlbum = "Test Album Name"
+ private val testArtist = "Test Artist Name"
+ private val testGenre = "Test Genre Name"
+ private val testImageName = "testimagename.jpg"
+ private val testDay = 1
+ private val testMonth = 1
+ private val testYear = 2018
+ private val testType: Int = SONG
+
+ @Before
+ fun setUp() {
+ database =
+ Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, AppDatabase::class.java)
+ .allowMainThreadQueries().build()
+ dao = database!!.dao()
+ }
+
+ @After
+ fun tearDown() {
+ database?.close()
+ }
+
+ private fun createTestItem(): FavouriteItem {
+ return FavouriteItem(testName, testAlbum, testArtist, testGenre, testDay, testMonth, testYear, testType, testImageName)
+ }
+
+ @Test
+ fun insertOneSong_getAllItems() {
+ val testItem = createTestItem()
+
+ val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ database?.dao()!!.insertOrUpdateItem(testItem)
+
+ val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(1, returnedItemsWhenOneInserted.size)
+ }
+
+ @Test
+ fun insertMultipleSongs_getAllItems() {
+ val testItem1 = createTestItem()
+ val testItem2 = createTestItem()
+
+ val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+
+ val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(2, returnedItemsWhenOneInserted.size)
+ }
+
+ @Test
+ fun insertOneSong_getItemById_deleteSong() {
+ val testId = 1
+
+ val testItem = createTestItem()
+ testItem.id = testId
+
+ val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getItemById(testId)
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ database?.dao()!!.insertOrUpdateItem(testItem)
+
+ val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getItemById(testId)
+ Assert.assertEquals(1, returnedItemsWhenOneInserted.size)
+
+ database?.dao()!!.deleteItemById(testId)
+
+ val returnedItemsWhenOneDeleted: MutableList = database?.dao()!!.getItemById(testId)
+ Assert.assertEquals(0, returnedItemsWhenOneDeleted.size)
+ }
+
+ @Test
+ fun insertTwoSongs_deleteOneSong() {
+ val testId1 = 1
+ val testId2 = 2
+
+ val testItem1 = createTestItem()
+ testItem1.id = testId1
+ val testItem2 = createTestItem()
+ testItem2.id = testId2
+
+ val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+
+ val returnedItemsWhenTwoInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(2, returnedItemsWhenTwoInserted.size)
+
+ database?.dao()!!.deleteItemById(testId1)
+
+ val returnedItemsWhenOneDeleted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(1, returnedItemsWhenOneDeleted.size)
+
+ val returnedItemsWhenDeletedOneSearched: MutableList = database?.dao()!!.getItemById(testId1)
+ Assert.assertEquals(0, returnedItemsWhenDeletedOneSearched.size)
+ }
+
+ @Test
+ fun insertMultipleSongs_getOneById() {
+ val testId1 = 1
+ val testId2 = 2
+
+ val testItem1 = createTestItem()
+ testItem1.id = testId1
+ val testItem2 = createTestItem()
+ testItem2.id = testId2
+
+ val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+
+ val returnedAllItemsWhenTwoInserted: MutableList = database?.dao()!!.getAllItems()
+ Assert.assertEquals(2, returnedAllItemsWhenTwoInserted.size)
+
+ val returnedItemsWhenSearchedById1: MutableList = database?.dao()!!.getItemById(testId1)
+ Assert.assertEquals(1, returnedItemsWhenSearchedById1.size)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEach() {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+ database?.dao()!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEachAndDelete() {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+ val testId1 = 1
+ val testId2 = 2
+ val testId3 = 3
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+ testItem1.id = testId1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+ testItem2.id = testId2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+ testItem3.id = testId3
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+ database?.dao()!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+
+ database?.dao()!!.deleteItemById(testId2)
+
+ val returnedValueWhenSearchedForTestImage1AfterDelete: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1AfterDelete)
+
+ val returnedValueWhenSearchedForTestImage2AfterDelete: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage2)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage2AfterDelete)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEachAndDeleteOneCompletely() {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+ val testId1 = 1
+ val testId2 = 2
+ val testId3 = 3
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+ testItem1.id = testId1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+ testItem2.id = testId2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+ testItem3.id = testId3
+
+ database?.dao()!!.insertOrUpdateItem(testItem1)
+ database?.dao()!!.insertOrUpdateItem(testItem2)
+ database?.dao()!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+
+ database?.dao()!!.deleteItemById(testId1)
+
+ val returnedValueWhenSearchedForTestImage1AfterDelete: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1)
+ Assert.assertEquals(0, returnedValueWhenSearchedForTestImage1AfterDelete)
+
+ val returnedValueWhenSearchedForTestImage2AfterDelete: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2AfterDelete)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt b/app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt
new file mode 100644
index 0000000..d56cfce
--- /dev/null
+++ b/app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt
@@ -0,0 +1,240 @@
+package app.marcdev.earworm.repository
+
+import androidx.room.Room
+import androidx.test.platform.app.InstrumentationRegistry
+import app.marcdev.earworm.database.AppDatabase
+import app.marcdev.earworm.database.DAO
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.SONG
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+
+class FavouriteItemRepositoryTest {
+
+ private var database: AppDatabase? = null
+ private var repository: FavouriteItemRepository? = null
+ private var dao: DAO? = null
+
+ // Default values
+ private val testName = "Test Song Name"
+ private val testAlbum = "Test Album Name"
+ private val testArtist = "Test Artist Name"
+ private val testGenre = "Test Genre Name"
+ private val testImageName = "testimagename.jpg"
+ private val testDay = 1
+ private val testMonth = 1
+ private val testYear = 2018
+ private val testType: Int = SONG
+
+ @Before
+ fun setUp() {
+ database =
+ Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, AppDatabase::class.java)
+ .allowMainThreadQueries().build()
+
+ dao = database!!.dao()
+ repository = FavouriteItemRepositoryImpl(dao!!)
+ }
+
+ @After
+ fun tearDown() {
+ database!!.close()
+ }
+
+ private fun createTestItem(): FavouriteItem {
+ return FavouriteItem(testName, testAlbum, testArtist, testGenre, testDay, testMonth, testYear, testType, testImageName)
+ }
+
+ @Test
+ fun insertMultipleSongs_getAllItems() = runBlocking {
+ val testItem1 = createTestItem()
+ val testItem2 = createTestItem()
+
+ val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+
+ val returnedItemsWhenOneInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(2, returnedItemsWhenOneInserted.size)
+ }
+
+ @Test
+ fun insertOneSong_getItemById_deleteSong() = runBlocking {
+ val testId = 1
+
+ val testItem = createTestItem()
+ testItem.id = testId
+
+ val returnedItemsWhenNothingInserted: MutableList = repository!!.getItem(testId)
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ repository!!.insertOrUpdateItem(testItem)
+
+ val returnedItemsWhenOneInserted: MutableList = repository!!.getItem(testId)
+ Assert.assertEquals(1, returnedItemsWhenOneInserted.size)
+
+ repository!!.deleteItem(testId)
+
+ val returnedItemsWhenOneDeleted: MutableList = repository!!.getItem(testId)
+ Assert.assertEquals(0, returnedItemsWhenOneDeleted.size)
+ }
+
+ @Test
+ fun insertTwoSongs_deleteOneSong() = runBlocking {
+ val testId1 = 1
+ val testId2 = 2
+
+ val testItem1 = createTestItem()
+ testItem1.id = testId1
+ val testItem2 = createTestItem()
+ testItem2.id = testId2
+
+ val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+
+ val returnedItemsWhenTwoInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(2, returnedItemsWhenTwoInserted.size)
+
+ repository!!.deleteItem(testId1)
+
+ val returnedItemsWhenOneDeleted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(1, returnedItemsWhenOneDeleted.size)
+
+ val returnedItemsWhenDeletedOneSearched: MutableList = repository!!.getItem(testId1)
+ Assert.assertEquals(0, returnedItemsWhenDeletedOneSearched.size)
+ }
+
+ @Test
+ fun insertMultipleSongs_getOneById() = runBlocking {
+ val testId1 = 1
+ val testId2 = 2
+
+ val testItem1 = createTestItem()
+ testItem1.id = testId1
+ val testItem2 = createTestItem()
+ testItem2.id = testId2
+
+ val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(0, returnedItemsWhenNothingInserted.size)
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+
+ val returnedAllItemsWhenTwoInserted: MutableList = repository!!.getAllItems()
+ Assert.assertEquals(2, returnedAllItemsWhenTwoInserted.size)
+
+ val returnedItemsWhenSearchedById1: MutableList = repository!!.getItem(testId1)
+ Assert.assertEquals(1, returnedItemsWhenSearchedById1.size)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEach() = runBlocking {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+ repository!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = repository!!.countUsesOfImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEachAndDelete() = runBlocking {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+ val testId1 = 1
+ val testId2 = 2
+ val testId3 = 3
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+ testItem1.id = testId1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+ testItem2.id = testId2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+ testItem3.id = testId3
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+ repository!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = repository!!.countUsesOfImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+
+ repository!!.deleteItem(testId2)
+
+ val returnedValueWhenSearchedForTestImage1AfterDelete: Int = repository!!.countUsesOfImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1AfterDelete)
+
+ val returnedValueWhenSearchedForTestImage2AfterDelete: Int = repository!!.countUsesOfImage(testImage2)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage2AfterDelete)
+ }
+
+ @Test
+ fun insertMultipleItemsUsingDifferentImages_countEachAndDeleteOneCompletely() = runBlocking {
+ val testImage1 = "testImage1.jpg"
+ val testImage2 = "testImage2.jpg"
+ val testId1 = 1
+ val testId2 = 2
+ val testId3 = 3
+
+ val testItem1 = createTestItem()
+ testItem1.imageName = testImage1
+ testItem1.id = testId1
+
+ val testItem2 = createTestItem()
+ testItem2.imageName = testImage2
+ testItem2.id = testId2
+
+ val testItem3 = createTestItem()
+ testItem3.imageName = testImage2
+ testItem3.id = testId3
+
+ repository!!.insertOrUpdateItem(testItem1)
+ repository!!.insertOrUpdateItem(testItem2)
+ repository!!.insertOrUpdateItem(testItem3)
+
+ val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1)
+ Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1)
+
+ val returnedValueWhenSearchedForTestImage2: Int = repository!!.countUsesOfImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2)
+
+ repository!!.deleteItem(testId1)
+
+ val returnedValueWhenSearchedForTestImage1AfterDelete: Int = repository!!.countUsesOfImage(testImage1)
+ Assert.assertEquals(0, returnedValueWhenSearchedForTestImage1AfterDelete)
+
+ val returnedValueWhenSearchedForTestImage2AfterDelete: Int = repository!!.countUsesOfImage(testImage2)
+ Assert.assertEquals(2, returnedValueWhenSearchedForTestImage2AfterDelete)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..98564a5
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..14f84b1
Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/app/marcdev/earworm/Earworm.kt b/app/src/main/java/app/marcdev/earworm/Earworm.kt
new file mode 100644
index 0000000..ccbdf72
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/Earworm.kt
@@ -0,0 +1,15 @@
+package app.marcdev.earworm
+
+import android.app.Application
+import timber.log.Timber
+
+class Earworm : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ Timber.i("Log: Timber Debug Tree planted")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/MainActivity.kt b/app/src/main/java/app/marcdev/earworm/MainActivity.kt
new file mode 100644
index 0000000..4a56954
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/MainActivity.kt
@@ -0,0 +1,67 @@
+package app.marcdev.earworm
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import app.marcdev.earworm.mainscreen.MainFragmentViewImpl
+import app.marcdev.earworm.utils.DARK_THEME
+import app.marcdev.earworm.utils.LIGHT_THEME
+import app.marcdev.earworm.utils.getTheme
+import app.marcdev.earworm.utils.setFragment
+import timber.log.Timber
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var mainFrame: CoordinatorLayout
+ private var activityTheme: Int = -1
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Timber.d("Log: onCreate: Started")
+
+ /* Theme changes must be done before super.onCreate otherwise it will be overridden with the value
+ in the manifest */
+ if(getTheme(applicationContext) == DARK_THEME) {
+ Timber.v("Log: onCreate: Is dark mode")
+ setTheme(R.style.Earworm_DarkTheme)
+ activityTheme = DARK_THEME
+ } else {
+ Timber.v("Log: onCreate: Is not dark mode")
+ setTheme(R.style.Earworm_LightTheme)
+ activityTheme = LIGHT_THEME
+ }
+
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ bindViews()
+
+ setDefaultFragment()
+ }
+
+ private fun bindViews() {
+ Timber.v("Log: bindViews: Started")
+ this.mainFrame = findViewById(R.id.frame_main)
+ }
+
+ private fun setDefaultFragment() {
+ Timber.v("Log: setDefaultFragment: Started")
+ val fragment = MainFragmentViewImpl()
+
+ if(intent.action == "app.marcdev.earworm.intent.ADD_ITEM") {
+ Timber.d("Log: onCreate: Started from Add Item app shortcut")
+ val args = Bundle()
+ args.putBoolean("add_item", true)
+ fragment.arguments = args
+ }
+
+ setFragment(fragment, supportFragmentManager, R.id.frame_main)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if(getTheme(applicationContext) != activityTheme) {
+ Timber.d("Log: onResume: Theme was changed, recreating activity")
+ recreate()
+ }
+ }
+}
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt
new file mode 100644
index 0000000..27b8660
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt
@@ -0,0 +1,383 @@
+package app.marcdev.earworm.additem
+
+import android.Manifest
+import android.app.Activity
+import android.app.Dialog
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.uicomponents.RoundedBottomDialogFragment
+import app.marcdev.earworm.utils.*
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.chip.Chip
+import droidninja.filepicker.FilePickerBuilder
+import droidninja.filepicker.FilePickerConst
+import timber.log.Timber
+import java.util.*
+
+class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView {
+
+ private lateinit var saveButton: MaterialButton
+ private lateinit var primaryInput: EditText
+ private lateinit var secondaryInput: EditText
+ private lateinit var songButton: ImageButton
+ private lateinit var albumButton: ImageButton
+ private lateinit var artistButton: ImageButton
+ private lateinit var presenter: AddItemPresenter
+ private lateinit var datePickerDialog: Dialog
+ private lateinit var datePicker: DatePicker
+ private lateinit var datePickerOk: MaterialButton
+ private lateinit var datePickerCancel: MaterialButton
+ private lateinit var iconImageView: ImageView
+ private lateinit var dateChip: Chip
+ private lateinit var confirmDeleteDialog: Dialog
+ private val dateChosen = Calendar.getInstance()
+ // If the itemID is anything other than -1 then it is in edit mode
+ private var itemId: Int = -1
+
+ private var type: Int = 0
+ private var recyclerUpdateView: RecyclerUpdateView? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val view = inflater.inflate(R.layout.dialog_add_item, container, false)
+ presenter = AddItemPresenterImpl(this, activity!!.applicationContext)
+ bindViews(view)
+
+ arguments?.let {
+ Timber.d("Log: onCreateView: Arguments not null")
+ this.itemId = arguments!!.getInt("item_id")
+ presenter.getItem(itemId)
+ } ?: run {
+ Timber.d("Log: onCreateView: Arguments null")
+ setupDefaults()
+ }
+ return view
+ }
+
+ override fun convertToEditMode(item: FavouriteItem) {
+ Timber.d("Log: convertToEditMode: Started")
+
+ when(item.type) {
+ SONG -> {
+ activateButton(songButton)
+ primaryInput.setText(item.songName)
+ secondaryInput.setText(item.artistName)
+ }
+
+ ALBUM -> {
+ activateButton(albumButton)
+ primaryInput.setText(item.albumName)
+ secondaryInput.setText(item.artistName)
+ }
+
+ ARTIST -> {
+ activateButton(artistButton)
+ primaryInput.setText(item.artistName)
+ secondaryInput.setText(item.genre)
+ }
+ }
+
+ if(item.imageName.isNotBlank()) {
+ presenter.updateFilePath(getArtworkDirectory(requireContext()) + item.imageName)
+ }
+ updateDateAndDisplay(item.day, item.month, item.year)
+ }
+
+ fun bindRecyclerUpdateView(view: RecyclerUpdateView) {
+ Timber.d("Log: bindRecyclerUpdateView: Started")
+ this.recyclerUpdateView = view
+ }
+
+ private fun bindViews(view: View) {
+ Timber.v("Log: bindViews: Started")
+ this.saveButton = view.findViewById(R.id.btn_add_item_save)
+ saveButton.setOnClickListener(saveOnClickListener)
+
+ this.primaryInput = view.findViewById(R.id.edt_item_primary_input)
+ this.secondaryInput = view.findViewById(R.id.edt_item_secondary_input)
+
+ this.songButton = view.findViewById(R.id.btn_add_item_song_choice)
+ songButton.setOnClickListener(songOnClickListener)
+
+ this.albumButton = view.findViewById(R.id.btn_add_item_album_choice)
+ albumButton.setOnClickListener(albumOnClickListener)
+
+ this.artistButton = view.findViewById(R.id.btn_add_item_artist_choice)
+ artistButton.setOnClickListener(artistOnClickListener)
+
+ this.datePickerDialog = Dialog(this.requireActivity())
+ datePickerDialog.setContentView(R.layout.dialog_datepicker)
+ datePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ this.datePicker = datePickerDialog.findViewById(R.id.datepicker)
+
+ this.datePickerOk = datePickerDialog.findViewById(R.id.btn_datepicker_ok)
+ datePickerOk.setOnClickListener(dateOnOkClickListener)
+
+ this.datePickerCancel = datePickerDialog.findViewById(R.id.btn_datepicker_cancel)
+ datePickerCancel.setOnClickListener(dateOnCancelClickListener)
+
+ this.dateChip = view.findViewById(R.id.chip_add_item_date_display)
+ dateChip.setOnClickListener(dateOnClickListener)
+
+ this.iconImageView = view.findViewById(R.id.img_add_icon)
+ iconImageView.setOnClickListener(iconOnClickListener)
+ iconImageView.setOnLongClickListener(iconOnLongClickListener)
+ // Convert to dark mode if needed
+ changeColorOfDrawable(requireContext(), iconImageView.drawable, false)
+
+ initEditDialog()
+ }
+
+ private val saveOnClickListener = View.OnClickListener {
+ Timber.d("Log: SaveClick: Clicked")
+ if(itemId == -1) {
+ Timber.d("Log: saveOnClickListener: Adding new item")
+ presenter.addItem(primaryInput.text.toString(), secondaryInput.text.toString(), type, dateChosen, null)
+ } else {
+ Timber.d("Log: saveOnClickListener: Updating item with id = $itemId")
+ presenter.addItem(primaryInput.text.toString(), secondaryInput.text.toString(), type, dateChosen, itemId)
+ }
+ }
+
+ private val dateOnClickListener = View.OnClickListener {
+ Timber.d("Log: DateClick: Clicked")
+ datePicker.updateDate(dateChosen.get(Calendar.YEAR), dateChosen.get(Calendar.MONTH), dateChosen.get(Calendar.DAY_OF_MONTH))
+ datePickerDialog.show()
+ }
+
+ private val dateOnOkClickListener = View.OnClickListener {
+ Timber.d("Log: DateOkClick: Clicked")
+
+ val day = datePicker.dayOfMonth
+ val monthRaw = datePicker.month
+ val year = datePicker.year
+
+ updateDateAndDisplay(day, monthRaw, year)
+
+ datePickerDialog.dismiss()
+ }
+
+ private val dateOnCancelClickListener = View.OnClickListener {
+ Timber.d("Log: DateCancelClick: Clicked")
+ datePickerDialog.dismiss()
+ }
+
+ private val songOnClickListener = View.OnClickListener {
+ Timber.d("Log: SongClick: Clicked")
+ activateButtonIfNecessary(songButton)
+ }
+
+ private val albumOnClickListener = View.OnClickListener {
+ Timber.d("Log: AlbumClick: Clicked")
+ activateButtonIfNecessary(albumButton)
+ }
+
+ private val artistOnClickListener = View.OnClickListener {
+ Timber.d("Log: ArtistClick: Clicked")
+ activateButtonIfNecessary(artistButton)
+ }
+
+ private val iconOnClickListener = View.OnClickListener {
+ Timber.d("Log: IconClick: Started")
+
+ if(ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ askForStoragePermissions()
+ } else {
+ FilePickerBuilder.instance.setMaxCount(1)
+ .setActivityTheme(R.style.Earworm_DarkTheme)
+ .setActivityTitle(resources.getString(R.string.choose_an_image))
+ .pickPhoto(this)
+ }
+ }
+
+ private val iconOnLongClickListener = View.OnLongClickListener {
+ Timber.d("Log: IconLongClick: Started")
+
+ confirmDeleteDialog.show()
+ return@OnLongClickListener true
+ }
+
+ private fun initEditDialog() {
+ this.confirmDeleteDialog = Dialog(requireContext())
+ confirmDeleteDialog.setContentView(R.layout.dialog_delete_image)
+ confirmDeleteDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ val confirmDeleteButton = confirmDeleteDialog.findViewById(R.id.btn_delete_image_confirm)
+ confirmDeleteButton.setOnClickListener(confirmDeleteOnClickListener)
+ val cancelButton = confirmDeleteDialog.findViewById(R.id.btn_delete_image_cancel)
+ cancelButton.setOnClickListener(cancelDeleteOnClickListener)
+ }
+
+ private val confirmDeleteOnClickListener = View.OnClickListener {
+ Timber.d("Log: ConfirmDelete: Clicked")
+ presenter.updateFilePath("")
+ confirmDeleteDialog.dismiss()
+ }
+
+ private val cancelDeleteOnClickListener = View.OnClickListener {
+ Timber.d("Log: CancelDelete: Clicked")
+ confirmDeleteDialog.dismiss()
+ }
+
+ private fun askForStoragePermissions() {
+ Timber.d("Log: askForStoragePermissions: Started")
+ ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ if(requestCode == FilePickerConst.REQUEST_CODE_PHOTO && resultCode == Activity.RESULT_OK && data != null) {
+ val photoPathArray = data.getStringArrayListExtra(FilePickerConst.KEY_SELECTED_MEDIA)
+ val photoPath = photoPathArray[0]
+ presenter.updateFilePath(photoPath)
+
+ displayImage(photoPath)
+ }
+ }
+
+ private fun setupDefaults() {
+ Timber.d("Log: setupDefaults: Started")
+ type = SONG
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, true)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false)
+ dateChip.text = resources.getString(R.string.today)
+ }
+
+ private fun activateButtonIfNecessary(button: ImageButton) {
+ Timber.d("Log: activateButtonIfNecessary: Started")
+
+ if(type == SONG && button == songButton
+ || type == ALBUM && button == albumButton
+ || type == ARTIST && button == artistButton
+ ) {
+ Timber.d("Log: activateButtonIfNecessary: No need to update")
+ } else {
+ activateButton(button)
+ }
+ }
+
+ private fun activateButton(button: ImageButton) {
+ Timber.d("Log: activateButtonIfNecessary: Activating button $button")
+
+ when(button) {
+ songButton -> {
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, true)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false)
+ type = SONG
+ primaryInput.hint = resources.getString(R.string.song_name)
+ secondaryInput.hint = resources.getString(R.string.artist)
+ }
+
+ albumButton -> {
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, false)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, true)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false)
+ type = ALBUM
+ primaryInput.hint = resources.getString(R.string.album)
+ secondaryInput.hint = resources.getString(R.string.artist)
+ }
+
+ artistButton -> {
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, false)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false)
+ changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, true)
+ type = ARTIST
+ primaryInput.hint = resources.getString(R.string.artist)
+ secondaryInput.hint = resources.getString(R.string.genre)
+ }
+ }
+
+ if(PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(PREF_CLEAR_INPUTS, resources.getString(R.string.yes)) == resources.getString(R.string.yes)) {
+ primaryInput.setText("")
+ secondaryInput.setText("")
+ primaryInput.requestFocus()
+ }
+ }
+
+ private fun updateDateAndDisplay(day: Int, month: Int, year: Int) {
+ val todayCalendar = Calendar.getInstance()
+ val todayDay = todayCalendar.get(Calendar.DAY_OF_MONTH)
+ val todayMonthRaw = todayCalendar.get(Calendar.MONTH)
+ val todayYear = todayCalendar.get(Calendar.YEAR)
+
+ if(day == todayDay
+ && month == todayMonthRaw
+ && year == todayYear
+ ) {
+ // Display "Today" on chip
+ datePickerDialog.dismiss()
+ dateChip.text = resources.getString(R.string.today)
+ setDate(todayDay, todayMonthRaw, todayYear)
+ } else {
+
+ val date = formatDateForDisplay(day, month, year)
+ dateChip.text = date
+
+ setDate(day, month, year)
+ datePickerDialog.dismiss()
+ }
+ }
+
+ private fun setDate(day: Int, month: Int, year: Int) {
+ Timber.d("Log: setDate: Started with day = $day, month = $month, year = $year")
+ dateChosen.set(Calendar.DAY_OF_MONTH, day)
+ dateChosen.set(Calendar.MONTH, month)
+ dateChosen.set(Calendar.YEAR, year)
+ }
+
+ override fun saveCallback() {
+ Timber.d("Log: saveCallback: Started")
+ if(recyclerUpdateView == null) {
+ Timber.e("Log: saveCallback: RecyclerUpdateView is null, cannot update recycler")
+ } else {
+ recyclerUpdateView!!.fillData()
+ }
+ Toast.makeText(activity, resources.getString(R.string.item_added), Toast.LENGTH_SHORT).show()
+ dismiss()
+ }
+
+ override fun displayImage(imagePath: String) {
+ Timber.d("Log: displayImage: Started with imagePath = $imagePath")
+
+ if(imagePath.isBlank()) {
+ Glide.with(this)
+ .load(resources.getDrawable(R.drawable.ic_add_a_photo_24px, null))
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(iconImageView)
+ } else {
+ Glide.with(this)
+ .load(imagePath)
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(iconImageView)
+ }
+ }
+
+ override fun displayEmptyToast() {
+ Timber.d("Log: displayEmptyToast: Started")
+ Toast.makeText(activity, resources.getString(R.string.empty), Toast.LENGTH_SHORT).show()
+ }
+
+ override fun displayErrorToast() {
+ Timber.d("Log: displayErrorToast: Started")
+ Toast.makeText(activity, resources.getString(R.string.error), Toast.LENGTH_SHORT).show()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt
new file mode 100644
index 0000000..28f1c8e
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt
@@ -0,0 +1,17 @@
+package app.marcdev.earworm.additem
+
+import app.marcdev.earworm.database.FavouriteItem
+import java.io.File
+
+interface AddItemModel {
+
+ fun addItemAsync(item: FavouriteItem)
+
+ fun getItemAsync(itemId: Int)
+
+ fun saveFileToAppStorage(file: File)
+
+ fun countUsesOfImage(filePath: String)
+
+ fun deleteImage(filePath: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt
new file mode 100644
index 0000000..75a5600
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt
@@ -0,0 +1,95 @@
+package app.marcdev.earworm.additem
+
+import android.content.Context
+import app.marcdev.earworm.database.AppDatabase
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.repository.FavouriteItemRepository
+import app.marcdev.earworm.repository.FavouriteItemRepositoryImpl
+import app.marcdev.earworm.utils.getArtworkDirectory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.io.File
+
+class AddItemModelImpl(private val presenter: AddItemPresenter, private val context: Context) : AddItemModel {
+
+ private var repository: FavouriteItemRepository
+
+ init {
+ val db: AppDatabase = AppDatabase.getDatabase(context)
+ repository = FavouriteItemRepositoryImpl(db.dao())
+ }
+
+ override fun addItemAsync(item: FavouriteItem) {
+ Timber.d("Log: addItemAsync: Started")
+
+ GlobalScope.launch(Dispatchers.Main) {
+ async(Dispatchers.IO) {
+ repository.insertOrUpdateItem(item)
+ }.await()
+
+ presenter.addItemCallback()
+ }
+ }
+
+ override fun getItemAsync(itemId: Int) {
+ Timber.d("Log: getItemAsync: Started")
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val item = async(Dispatchers.IO) {
+ repository.getItem(itemId)
+ }.await()
+
+ presenter.getItemCallback(item)
+ }
+ }
+
+ override fun saveFileToAppStorage(file: File) {
+ Timber.d("Log: saveFileToAppStorage: Started")
+ val toPath = getArtworkDirectory(context) + file.name
+ Timber.d("Log: saveFileToAppStorage: fileName = ${file.name}")
+ val toFile = File(toPath)
+ if(toFile.compareTo(file) != 0) {
+ Timber.d("Log: saveFileToAppStorage: External file, saving")
+ try {
+ file.copyTo(toFile, true)
+ presenter.saveFileToAppStorageCallback(file.name, null)
+ } catch(e: NoSuchFileException) {
+ presenter.saveFileToAppStorageCallback("", e)
+ Timber.e("Log: saveFileToAppStorage: $e")
+ }
+ } else {
+ Timber.d("Log: saveFileToAppStorage: Internal file of same path, not saving")
+ presenter.saveFileToAppStorageCallback(file.name, null)
+ }
+ }
+
+ override fun countUsesOfImage(filePath: String) {
+ Timber.d("Log: countUsesOfImage: Started with filePath = $filePath")
+ val file = File(filePath)
+ val fileName = file.name
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val uses = async(Dispatchers.IO) {
+ repository.countUsesOfImage(fileName)
+ }.await()
+
+ presenter.countUsesOfImageCallback(filePath, uses)
+ }
+ }
+
+ override fun deleteImage(filePath: String) {
+ Timber.d("Log: deleteImage: Started with filePath = $filePath")
+
+ val file = File(filePath)
+ if(file.exists()) {
+ Timber.d("Log: deleteImage: File exists, deleting")
+ val deletionStatus = file.delete()
+ Timber.d("Log: deleteImage: Deletion: $deletionStatus")
+ } else {
+ Timber.w("Log: deleteImage: File doesn't exist")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt
new file mode 100644
index 0000000..3361d06
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt
@@ -0,0 +1,21 @@
+package app.marcdev.earworm.additem
+
+import app.marcdev.earworm.database.FavouriteItem
+import java.util.*
+
+interface AddItemPresenter {
+
+ fun addItem(primaryInput: String, secondaryInput: String, type: Int, dateChosen: Calendar, itemId: Int?)
+
+ fun addItemCallback()
+
+ fun getItem(itemId: Int)
+
+ fun getItemCallback(items: MutableList)
+
+ fun saveFileToAppStorageCallback(fileName: String, exception: NoSuchFileException?)
+
+ fun updateFilePath(filePath: String)
+
+ fun countUsesOfImageCallback(filePath: String, uses: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt
new file mode 100644
index 0000000..d7ff254
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt
@@ -0,0 +1,119 @@
+package app.marcdev.earworm.additem
+
+import android.content.Context
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.ALBUM
+import app.marcdev.earworm.utils.ARTIST
+import app.marcdev.earworm.utils.SONG
+import app.marcdev.earworm.utils.getArtworkDirectory
+import timber.log.Timber
+import java.io.File
+import java.util.*
+
+class AddItemPresenterImpl(private val view: AddItemView, private val context: Context) : AddItemPresenter {
+
+ private val model: AddItemModel
+ private var imageFilePath: String = ""
+ private var oldImageFilePath: String = ""
+
+ init {
+ model = AddItemModelImpl(this, context)
+ }
+
+ override fun addItem(primaryInput: String, secondaryInput: String, type: Int, dateChosen: Calendar, itemId: Int?) {
+ Timber.d("Log: addItem: Started")
+
+ if(primaryInput.isBlank() || secondaryInput.isBlank()) {
+ Timber.d("Log: addItem: Empty input")
+ view.displayEmptyToast()
+ } else {
+ Timber.d("Log: addItem: Adding item")
+ val day = dateChosen.get(Calendar.DAY_OF_MONTH)
+ val month = dateChosen.get(Calendar.MONTH)
+ val year = dateChosen.get(Calendar.YEAR)
+
+ var imageName = ""
+
+ if(imageFilePath.isNotBlank()) {
+ Timber.d("Log: addItem: imageFilePath = $imageFilePath")
+
+ val imageFile = File(imageFilePath)
+ imageName = imageFile.name
+ model.saveFileToAppStorage(imageFile)
+ }
+
+ val item: FavouriteItem = when(type) {
+ SONG -> FavouriteItem(primaryInput, "", secondaryInput, "", day, month, year, type, imageName)
+ ALBUM -> FavouriteItem("", primaryInput, secondaryInput, "", day, month, year, type, imageName)
+ ARTIST -> FavouriteItem("", "", primaryInput, secondaryInput, day, month, year, type, imageName)
+ else -> FavouriteItem("", "", "", "", 0, 0, 0, type, imageName)
+ }
+
+ if(itemId != null) {
+ Timber.d("Log: addItem: Updating old item with ID = $itemId")
+ item.id = itemId
+ }
+
+ model.addItemAsync(item)
+ }
+ }
+
+ override fun addItemCallback() {
+ Timber.d("Log: addItemCallback: Started")
+ view.saveCallback()
+ }
+
+ override fun getItem(itemId: Int) {
+ Timber.d("Log: getItem: Started")
+ model.getItemAsync(itemId)
+ }
+
+ override fun getItemCallback(items: MutableList) {
+ Timber.d("Log: getItemCallback: Started")
+
+ if(items.isNotEmpty()) {
+ view.convertToEditMode(items.first())
+ if(items.first().imageName.isNotBlank()) {
+ view.displayImage(getArtworkDirectory(context) + items.first().imageName)
+ }
+ } else {
+ Timber.e("Log: getItemCallback: Returned empty list")
+ }
+ }
+
+ override fun saveFileToAppStorageCallback(fileName: String, exception: NoSuchFileException?) {
+ Timber.d("Log: saveFileToAppStorageCallback: Started")
+ if(exception != null) {
+ view.displayErrorToast()
+ }
+ }
+
+ override fun updateFilePath(filePath: String) {
+ Timber.d("Log: updateFilePath: Started")
+
+ this.oldImageFilePath = imageFilePath
+ this.imageFilePath = filePath
+ Timber.d("Log: updateFilePath: oldImageFilePath = $oldImageFilePath")
+ Timber.d("Log: updateFilePath: imageFilePath = $imageFilePath")
+
+ if(oldImageFilePath.isNotBlank()) {
+ model.countUsesOfImage(oldImageFilePath)
+ }
+
+ if(imageFilePath.isBlank()) {
+ view.displayImage("")
+ model.countUsesOfImage(oldImageFilePath)
+ }
+ }
+
+ override fun countUsesOfImageCallback(filePath: String, uses: Int) {
+ Timber.d("Log: countUsesOfImageCallback: Started with filePath = $filePath and uses = $uses")
+
+ if(uses <= 1) {
+ Timber.d("Log: countUsesOfImageCallback: No other uses, deleting image at $filePath")
+ model.deleteImage(filePath)
+ } else {
+ Timber.d("Log: countUsesOfImageCallback: Used elsewhere, not deleting")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt
new file mode 100644
index 0000000..aa06a9d
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt
@@ -0,0 +1,16 @@
+package app.marcdev.earworm.additem
+
+import app.marcdev.earworm.database.FavouriteItem
+
+interface AddItemView {
+
+ fun saveCallback()
+
+ fun displayEmptyToast()
+
+ fun convertToEditMode(item: FavouriteItem)
+
+ fun displayErrorToast()
+
+ fun displayImage(imagePath: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt b/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt
new file mode 100644
index 0000000..55045bc
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt
@@ -0,0 +1,5 @@
+package app.marcdev.earworm.additem
+
+interface RecyclerUpdateView {
+ fun fillData()
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt b/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt
new file mode 100644
index 0000000..3cb8774
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt
@@ -0,0 +1,34 @@
+package app.marcdev.earworm.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+@Database(entities = [FavouriteItem::class], version = 4)
+abstract class AppDatabase : RoomDatabase() {
+
+ abstract fun dao(): DAO
+
+ companion object {
+ @Volatile
+ private var INSTANCE: AppDatabase? = null
+
+ fun getDatabase(context: Context): AppDatabase {
+ val tempInstance = INSTANCE
+
+ if(tempInstance != null) {
+ return tempInstance
+ }
+
+ synchronized(this) {
+
+ val instance = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "AppDatabase.db")
+ .fallbackToDestructiveMigration().build()
+
+ INSTANCE = instance
+ return instance
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/database/DAO.kt b/app/src/main/java/app/marcdev/earworm/database/DAO.kt
new file mode 100644
index 0000000..b799ce7
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/database/DAO.kt
@@ -0,0 +1,25 @@
+package app.marcdev.earworm.database
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+@Dao
+interface DAO {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertOrUpdateItem(item: FavouriteItem)
+
+ @Query("SELECT * FROM favourite_items")
+ fun getAllItems(): MutableList
+
+ @Query("SELECT * FROM favourite_items where id = :id")
+ fun getItemById(id: Int): MutableList
+
+ @Query("DELETE FROM favourite_items where id = :id")
+ fun deleteItemById(id: Int)
+
+ @Query("SELECT COUNT(*) FROM favourite_items WHERE imageName = :imageName")
+ fun getNumberOfEntriesUsingImage(imageName: String): Int
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt b/app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt
new file mode 100644
index 0000000..45fb289
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt
@@ -0,0 +1,21 @@
+package app.marcdev.earworm.database
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "favourite_items")
+data class FavouriteItem(
+ var songName: String,
+ var albumName: String,
+ var artistName: String,
+ var genre: String,
+ var day: Int,
+ var month: Int,
+ var year: Int,
+ var type: Int,
+ var imageName: String
+) {
+
+ @PrimaryKey(autoGenerate = true)
+ var id: Int? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt
new file mode 100644
index 0000000..b6ac979
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt
@@ -0,0 +1,17 @@
+package app.marcdev.earworm.mainscreen
+
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.ItemFilter
+
+interface MainFragmentModel {
+
+ fun getAllItemsAsync()
+
+ fun getAllItemsAsync(filter: ItemFilter)
+
+ fun deleteItemAsync(item: FavouriteItem)
+
+ fun countUsesOfImage(item: FavouriteItem)
+
+ fun deleteImage(filePath: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt
new file mode 100644
index 0000000..27eccdd
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt
@@ -0,0 +1,89 @@
+package app.marcdev.earworm.mainscreen
+
+import android.content.Context
+import app.marcdev.earworm.database.AppDatabase
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.repository.FavouriteItemRepository
+import app.marcdev.earworm.repository.FavouriteItemRepositoryImpl
+import app.marcdev.earworm.utils.ItemFilter
+import app.marcdev.earworm.utils.getArtworkDirectory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.io.File
+
+class MainFragmentModelImpl(private val presenter: MainFragmentPresenter, private val context: Context) : MainFragmentModel {
+
+ private var repository: FavouriteItemRepository
+
+ init {
+ val db: AppDatabase = AppDatabase.getDatabase(context)
+ repository = FavouriteItemRepositoryImpl(db.dao())
+ }
+
+ override fun getAllItemsAsync() {
+ Timber.d("Log: getAllItemsAsync: Started")
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val allItems = async(Dispatchers.IO) {
+ repository.getAllItems()
+ }.await()
+
+ presenter.getAllItemsCallback(allItems)
+ }
+ }
+
+ override fun getAllItemsAsync(filter: ItemFilter) {
+ Timber.d("Log: getAllItemsAsync: Started")
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val allItems = async(Dispatchers.IO) {
+ repository.getAllItems()
+ }.await()
+
+ presenter.getAllItemsCallback(allItems, filter)
+ }
+ }
+
+ override fun deleteItemAsync(item: FavouriteItem) {
+ Timber.d("Log: deleteItemAsync: Started")
+
+ GlobalScope.launch(Dispatchers.Main) {
+ async(Dispatchers.IO) {
+ repository.deleteItem(item.id!!)
+ }.await()
+
+ presenter.deleteItemCallback()
+ }
+ }
+
+ override fun countUsesOfImage(item: FavouriteItem) {
+ Timber.d("Log: countUsesOfImage: Started with item = $item")
+ val filePath = getArtworkDirectory(context) + item.imageName
+ val file = File(filePath)
+ val fileName = file.name
+
+ GlobalScope.launch(Dispatchers.Main) {
+ val uses = async(Dispatchers.IO) {
+ repository.countUsesOfImage(fileName)
+ }.await()
+
+ presenter.countUsesOfImageCallback(item, uses)
+ }
+ }
+
+ override fun deleteImage(filePath: String) {
+ Timber.d("Log: deleteImage: Started with filePath = $filePath")
+
+ val file = File(filePath)
+ if(file.exists()) {
+ Timber.d("Log: deleteImage: File exists, deleting")
+ val deletionStatus = file.delete()
+ Timber.d("Log: deleteImage: Deletion: $deletionStatus")
+ } else {
+ Timber.w("Log: deleteImage: File doesn't exist")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt
new file mode 100644
index 0000000..8aae525
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt
@@ -0,0 +1,25 @@
+package app.marcdev.earworm.mainscreen
+
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.ItemFilter
+
+interface MainFragmentPresenter {
+
+ fun getAllItems()
+
+ fun getAllItems(filter: ItemFilter)
+
+ fun getAllItemsCallback(items: MutableList)
+
+ fun getAllItemsCallback(items: MutableList, filter: ItemFilter)
+
+ fun deleteItem(item: FavouriteItem)
+
+ fun deleteItemCallback()
+
+ fun editItemClick(itemId: Int)
+
+ fun search(input: String)
+
+ fun countUsesOfImageCallback(item: FavouriteItem, uses: Int)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt
new file mode 100644
index 0000000..b8cd93c
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt
@@ -0,0 +1,109 @@
+package app.marcdev.earworm.mainscreen
+
+import android.content.Context
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.*
+import timber.log.Timber
+
+class MainFragmentPresenterImpl(val view: MainFragmentView, val context: Context) : MainFragmentPresenter {
+ private var model = MainFragmentModelImpl(this, context)
+
+ override fun getAllItems() {
+ Timber.d("Log: getAllItems: Started")
+ model.getAllItemsAsync()
+ }
+
+ override fun getAllItems(filter: ItemFilter) {
+ Timber.d("Log: getAllItems with Filter: Started")
+ Timber.i("Log: getAllItems: Input Filter = $filter")
+ if(filter != DEFAULT_FILTER.copy()) {
+ view.activateFilterIcon(true)
+ } else {
+ view.activateFilterIcon(false)
+ }
+ model.getAllItemsAsync(filter)
+ }
+
+ override fun getAllItemsCallback(items: MutableList) {
+ Timber.d("Log: getAllItemsCallback: Started")
+
+ val sortedItems = sortByDateDescending(items)
+ val itemsWithHeaders = addListHeaders(sortedItems)
+
+ view.updateRecycler(itemsWithHeaders)
+ view.displayProgress(false)
+
+ if(itemsWithHeaders.isEmpty()) {
+ view.displayNoEntriesWarning(true)
+ } else {
+ view.displayNoEntriesWarning(false)
+ }
+ }
+
+ override fun getAllItemsCallback(items: MutableList, filter: ItemFilter) {
+ Timber.d("Log: getAllItemsCallback: Started with filter = $filter")
+
+ val sortedItems = applyFilter(items, filter)
+ val itemsWithHeaders = addListHeaders(sortedItems)
+
+ view.updateRecycler(itemsWithHeaders)
+ view.displayProgress(false)
+
+ if(itemsWithHeaders.isEmpty()) {
+ view.displayNoFilteredResultsWarning(true)
+ } else {
+ view.displayNoFilteredResultsWarning(false)
+ }
+ }
+
+ override fun deleteItem(item: FavouriteItem) {
+ Timber.d("Log: deleteItem: Started")
+ if(item.imageName != "") {
+ Timber.d("Log: deleteItem: Item has an image, checking if used elsewhere")
+ model.countUsesOfImage(item)
+ } else {
+ Timber.d("Log: deleteItem: Item does not have an image, deleting item")
+ model.deleteItemAsync(item)
+ }
+ }
+
+ override fun deleteItemCallback() {
+ Timber.d("Log: deleteItemCallback: Started")
+ if(view.getActiveFilter() != DEFAULT_FILTER.copy()) {
+ getAllItems(view.getActiveFilter())
+ } else {
+ getAllItems()
+ }
+ }
+
+ override fun countUsesOfImageCallback(item: FavouriteItem, uses: Int) {
+ Timber.d("Log: countUsesOfImageCallback: Started with imageName = ${item.imageName} and uses = $uses")
+
+ if(uses <= 1) {
+ val filePath = getArtworkDirectory(context) + item.imageName
+ model.deleteImage(filePath)
+ }
+
+ model.deleteItemAsync(item)
+ }
+
+ override fun editItemClick(itemId: Int) {
+ Timber.d("Log: editItemClick: Started")
+ view.displayEditItemSheet(itemId)
+ }
+
+ override fun search(input: String) {
+ Timber.d("Log: search: Started with input = $input")
+ if(input.isBlank()) {
+ val inputFilter = view.getActiveFilter()
+ inputFilter.searchTerm = ""
+ model.getAllItemsAsync(inputFilter)
+ view.changeSearchIcon(true)
+ } else {
+ val inputFilter = view.getActiveFilter()
+ inputFilter.searchTerm = input.trim()
+ model.getAllItemsAsync(inputFilter)
+ view.changeSearchIcon(false)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt
new file mode 100644
index 0000000..86660e2
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt
@@ -0,0 +1,27 @@
+package app.marcdev.earworm.mainscreen
+
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.ItemFilter
+
+interface MainFragmentView {
+
+ fun displayNoEntriesWarning(display: Boolean)
+
+ fun displayNoFilteredResultsWarning(display: Boolean)
+
+ fun displayAddedToast()
+
+ fun displayItemDeletedToast()
+
+ fun updateRecycler(items: List)
+
+ fun displayProgress(isVisible: Boolean)
+
+ fun displayEditItemSheet(itemId: Int)
+
+ fun getActiveFilter(): ItemFilter
+
+ fun changeSearchIcon(isSearch: Boolean)
+
+ fun activateFilterIcon(isActive: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt
new file mode 100644
index 0000000..0024ef0
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt
@@ -0,0 +1,273 @@
+package app.marcdev.earworm.mainscreen
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.core.widget.NestedScrollView
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.additem.AddItemBottomSheet
+import app.marcdev.earworm.additem.RecyclerUpdateView
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.mainscreen.mainrecycler.MainRecyclerAdapter
+import app.marcdev.earworm.settingsscreen.SettingsActivity
+import app.marcdev.earworm.uicomponents.FilterDialog
+import app.marcdev.earworm.utils.*
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import timber.log.Timber
+
+class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView {
+
+ private lateinit var fab: FloatingActionButton
+ private lateinit var noEntriesWarning: TextView
+ private lateinit var noEntriesWarningImage: ImageView
+ private lateinit var noFilteredResultsWarning: TextView
+ private lateinit var noFilteredResultsWarningImage: ImageView
+ private lateinit var progressBar: ProgressBar
+ private lateinit var searchInput: EditText
+ private lateinit var searchButton: ImageButton
+ private lateinit var filterButton: ImageButton
+ private lateinit var settingsButton: ImageButton
+ private lateinit var filterDialog: FilterDialog
+ private lateinit var recyclerAdapter: MainRecyclerAdapter
+ private lateinit var presenter: MainFragmentPresenter
+ private var isSearchMode = true
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ Timber.d("Log: onCreateView: Started")
+ val view = inflater.inflate(R.layout.fragment_mainscreen, container, false)
+
+ presenter = MainFragmentPresenterImpl(this, activity!!.applicationContext)
+ bindViews(view)
+
+ if(getTheme(requireContext()) == DARK_THEME) {
+ Timber.d("Log: onCreateView: Is dark mode, converting")
+ convertToDarkMode()
+ }
+
+ setupRecycler(view)
+ fillData()
+
+ // If arguments is not null, see if the app has been opened from an app shortcut
+ arguments?.let {
+ if(arguments!!.getBoolean("add_item", false)) {
+ fabOnClickListener.onClick(view)
+ }
+ }
+
+ return view
+ }
+
+ private fun bindViews(view: View) {
+ Timber.v("Log: bindViews: Started")
+ this.fab = view.findViewById(R.id.fab_main)
+ fab.setOnClickListener(fabOnClickListener)
+
+ this.noEntriesWarning = view.findViewById(R.id.txt_noEntries)
+ this.noEntriesWarningImage = view.findViewById(R.id.img_noEntries)
+
+ this.noFilteredResultsWarning = view.findViewById(R.id.txt_noFilteredResults)
+ this.noFilteredResultsWarning.visibility = View.GONE
+ this.noFilteredResultsWarningImage = view.findViewById(R.id.img_noFilteredResults)
+ this.noFilteredResultsWarningImage.visibility = View.GONE
+
+ this.progressBar = view.findViewById(R.id.prog_main)
+
+ this.searchInput = view.findViewById(R.id.edt_filter_input)
+ searchInput.setOnKeyListener(searchOnEnterListener)
+ searchInput.addTextChangedListener(searchOnTextChangedListener)
+
+ this.searchButton = view.findViewById(R.id.img_search)
+ searchButton.setOnClickListener(searchOnClickListener)
+
+ this.filterButton = view.findViewById(R.id.img_filter)
+ filterButton.setOnClickListener(filterOnClickListener)
+
+ val nestedScrollView: NestedScrollView = view.findViewById(R.id.scroll_main)
+ nestedScrollView.setOnScrollChangeListener(scrollViewOnScrollChangeListener)
+
+ this.filterDialog = FilterDialog(requireActivity(), presenter)
+
+ this.settingsButton = view.findViewById(R.id.img_settings)
+ settingsButton.setOnClickListener(settingsOnClickListener)
+ }
+
+ private val fabOnClickListener = View.OnClickListener {
+ Timber.d("Log: Fab Clicked")
+ val addDialog = AddItemBottomSheet()
+ addDialog.bindRecyclerUpdateView(this)
+ addDialog.show(fragmentManager, "Add Item Bottom Sheet Dialog")
+ }
+
+ private val settingsOnClickListener = View.OnClickListener {
+ Timber.d("Log: Settings Clicked")
+ val intent = Intent(requireContext(), SettingsActivity::class.java)
+ startActivity(intent)
+ }
+
+ private val searchOnEnterListener: View.OnKeyListener = View.OnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
+ testIfSubmitButtonClicked(keyEvent, keyCode)
+ }
+
+ private val searchOnTextChangedListener = object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) { /* Necessary */
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* Necessary */
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ if(s.isNullOrBlank()) {
+ presenter.search("")
+ }
+ }
+ }
+
+ private fun testIfSubmitButtonClicked(keyEvent: KeyEvent, keyCode: Int): Boolean {
+ Timber.d("Log: testIfSubmitButtonClicked: Submit button clicked")
+ if((keyEvent.action == KeyEvent.ACTION_DOWN) && keyCode == KeyEvent.KEYCODE_ENTER) {
+ presenter.search(searchInput.text.toString())
+ return true
+ }
+ return false
+ }
+
+ private val searchOnClickListener = View.OnClickListener {
+ Timber.d("Log: Search Clicked")
+ if(isSearchMode) {
+ presenter.search(searchInput.text.toString())
+ } else {
+ searchInput.setText("")
+ }
+ }
+
+ private val filterOnClickListener = View.OnClickListener {
+ Timber.d("Log: Filter Clicked")
+ filterDialog.show()
+ }
+
+ private var scrollViewOnScrollChangeListener = { _: View, _: Int, scrollY: Int, _: Int, oldScrollY: Int -> hideFabOnScroll(scrollY, oldScrollY) }
+
+ private fun hideFabOnScroll(scrollY: Int, oldScrollY: Int) {
+ if(scrollY > oldScrollY) {
+ fab.hide()
+ } else {
+ fab.show()
+ }
+ }
+
+ private fun setupRecycler(view: View) {
+ Timber.v("Log: setupRecycler: Started")
+ val recycler: RecyclerView = view.findViewById(R.id.recycler_main)
+ this.recyclerAdapter = MainRecyclerAdapter(context, presenter)
+ recycler.adapter = recyclerAdapter
+ recycler.layoutManager = LinearLayoutManager(context)
+ }
+
+ override fun fillData() {
+ Timber.d("Log: fillData: Started")
+ displayProgress(true)
+ displayNoEntriesWarning(false)
+ presenter.getAllItems()
+ }
+
+ override fun displayNoEntriesWarning(display: Boolean) {
+ Timber.d("Log: displayNoEntriesWarning: Started with display = $display")
+
+ if(display) {
+ Timber.d("Log: displayNoEntriesWarning: Displaying")
+ noEntriesWarning.visibility = View.VISIBLE
+ noEntriesWarningImage.visibility = View.VISIBLE
+ } else {
+ Timber.d("Log: displayNoEntriesWarning: Hiding")
+ noEntriesWarning.visibility = View.GONE
+ noEntriesWarningImage.visibility = View.GONE
+ }
+ }
+
+ override fun updateRecycler(items: List) {
+ Timber.d("Log: updateRecycler: Started")
+ recyclerAdapter.updateItems(items)
+ }
+
+ override fun displayAddedToast() {
+ Timber.d("Log: displayAddedToast: Started")
+ Toast.makeText(activity, resources.getString(R.string.item_added), Toast.LENGTH_SHORT).show()
+ }
+
+ override fun displayItemDeletedToast() {
+ Timber.d("Log: displayItemDeletedToast: Started")
+ Toast.makeText(activity, resources.getString(R.string.item_deleted), Toast.LENGTH_SHORT).show()
+ }
+
+ override fun displayProgress(isVisible: Boolean) {
+ Timber.d("Log: displayProgress: Started with isVisible = $isVisible")
+ if(isVisible) {
+ progressBar.visibility = View.VISIBLE
+ } else {
+ progressBar.visibility = View.GONE
+ }
+ }
+
+ override fun displayEditItemSheet(itemId: Int) {
+ Timber.d("Log: displayEditItemSheet: Started with itemId = $itemId")
+ val addDialog = AddItemBottomSheet()
+ addDialog.bindRecyclerUpdateView(this)
+
+ val args = Bundle()
+ args.putInt("item_id", itemId)
+ addDialog.arguments = args
+
+ addDialog.show(fragmentManager, "Add Item Bottom Sheet Dialog")
+ }
+
+ override fun displayNoFilteredResultsWarning(display: Boolean) {
+ Timber.d("Log: displayNoFilteredResultsWarning: Started with display = $display")
+
+ if(display && (noEntriesWarning.visibility == View.GONE)) {
+ this.noFilteredResultsWarning.visibility = View.VISIBLE
+ this.noFilteredResultsWarningImage.visibility = View.VISIBLE
+ } else {
+ this.noFilteredResultsWarning.visibility = View.GONE
+ this.noFilteredResultsWarningImage.visibility = View.GONE
+ }
+ }
+
+ override fun getActiveFilter(): ItemFilter {
+ Timber.d("Log: getActiveFilter: Started")
+ return filterDialog.activeFilter
+ }
+
+ override fun changeSearchIcon(isSearch: Boolean) {
+ Timber.d("Log: changeSearchIcon: Started")
+ if(isSearch) {
+ searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_search_24px, null))
+ this.isSearchMode = true
+ } else {
+ searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_close_24px, null))
+ this.isSearchMode = false
+ }
+ }
+
+ override fun activateFilterIcon(isActive: Boolean) {
+ Timber.d("Log: activateFilterIcon: Started with isActive = $isActive")
+
+ changeColorOfImageButtonDrawable(context!!, filterButton, isActive)
+ }
+
+ private fun convertToDarkMode() {
+ changeColorOfImageButtonDrawable(requireContext(), filterButton, false)
+ changeColorOfImageButtonDrawable(requireContext(), settingsButton, false)
+ changeColorOfImageButtonDrawable(requireContext(), searchButton, false)
+ changeColorOfDrawable(requireContext(), noEntriesWarningImage.drawable, false)
+ changeColorOfDrawable(requireContext(), noFilteredResultsWarningImage.drawable, false)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt
new file mode 100644
index 0000000..7c6bce7
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt
@@ -0,0 +1,78 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.mainscreen.MainFragmentPresenter
+import app.marcdev.earworm.utils.*
+import timber.log.Timber
+
+class MainRecyclerAdapter(context: Context?, private val presenter: MainFragmentPresenter) : RecyclerView.Adapter(), MainRecyclerView {
+
+ private var items: List = mutableListOf()
+ private var inflater: LayoutInflater = LayoutInflater.from(context)
+
+ override fun getItemViewType(position: Int): Int {
+ Timber.v("Log: getItemViewType: Started")
+ return items[position].type
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder {
+ Timber.v("Log: onCreateViewHolder: Started")
+
+ lateinit var viewHolder: MainRecyclerViewHolder
+
+ when(viewType) {
+ HEADER -> {
+ Timber.v("Log: onCreateViewHolder: Type == Header")
+ val view = inflater.inflate(R.layout.item_header, parent, false)
+ viewHolder = MainRecyclerViewHolderHeader(view)
+ }
+ SONG -> {
+ Timber.v("Log: onCreateViewHolder: Type == Song")
+ val view = inflater.inflate(R.layout.item_mainrecycler_song, parent, false)
+ viewHolder = MainRecyclerViewHolderSong(view)
+ }
+
+ ALBUM -> {
+ Timber.v("Log: onCreateViewHolder: Type == Album")
+ val view = inflater.inflate(R.layout.item_mainrecycler_album, parent, false)
+ viewHolder = MainRecyclerViewHolderAlbum(view)
+ }
+
+ ARTIST -> {
+ Timber.v("Log: onCreateViewHolder: Type == Artist")
+ val view = inflater.inflate(R.layout.item_mainrecycler_artist, parent, false)
+ viewHolder = MainRecyclerViewHolderArtist(view)
+ }
+
+ GENRE -> {
+ Timber.v("Log: onCreateViewHolder: Type == Genre")
+ val view = inflater.inflate(R.layout.item_mainrecycler_genre, parent, false)
+ viewHolder = MainRecyclerViewHolderGenre(view)
+ }
+ }
+
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: MainRecyclerViewHolder, position: Int) {
+ Timber.v("Log: onBindViewHolder: $position")
+ holder.display(items[position])
+ holder.bindPresenterAndItem(presenter, items[position])
+ }
+
+ override fun getItemCount(): Int {
+ Timber.v("Log: getItemCount: ${items.size}")
+ return items.size
+ }
+
+ override fun updateItems(items: List) {
+ Timber.d("Log: updateItems: Started")
+ this.items = items
+ notifyDataSetChanged()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt
new file mode 100644
index 0000000..27a2735
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt
@@ -0,0 +1,7 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import app.marcdev.earworm.database.FavouriteItem
+
+interface MainRecyclerView {
+ fun updateItems(items: List)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt
new file mode 100644
index 0000000..ae2a229
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt
@@ -0,0 +1,78 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.preference.PreferenceManager
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.mainscreen.MainFragmentPresenter
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.snackbar.Snackbar
+import timber.log.Timber
+
+open class MainRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+ private lateinit var displayedItem: FavouriteItem
+ private lateinit var presenter: MainFragmentPresenter
+ private lateinit var editDialog: Dialog
+ private val prefs = PreferenceManager.getDefaultSharedPreferences(itemView.context)
+
+ private val snackbarActionListener = View.OnClickListener {
+ Timber.d("Log: snackbarActionListener: Clicked")
+ prefs.edit().putBoolean("pref_show_tips", false).apply()
+ }
+
+ private val itemClickListener = View.OnClickListener {
+ if(prefs.getBoolean("pref_show_tips", true)) {
+ val snackbar = Snackbar.make(it, itemView.resources.getString(R.string.long_click_hint), Snackbar.LENGTH_SHORT)
+ snackbar.setAction(itemView.resources.getString(R.string.dont_show), snackbarActionListener)
+ snackbar.show()
+ }
+ }
+
+ private val itemLongClickListener = View.OnLongClickListener {
+ editDialog.show()
+ return@OnLongClickListener true
+ }
+
+ private val editOnClickListener = View.OnClickListener {
+ Timber.d("Log: editOnClickListener: Clicked")
+ editDialog.dismiss()
+ presenter.editItemClick(displayedItem.id!!)
+ }
+
+ private val deleteOnClickListener = View.OnClickListener {
+ Timber.d("Log: deleteOnClickListener: Clicked")
+ presenter.deleteItem(displayedItem)
+ editDialog.dismiss()
+ }
+
+ init {
+ initEditDialog()
+ itemView.setOnClickListener(itemClickListener)
+ itemView.setOnLongClickListener(itemLongClickListener)
+ }
+
+ open fun display(favouriteItemToDisplay: FavouriteItem) {
+ // TO BE OVERRIDDEN
+ }
+
+ fun bindPresenterAndItem(presenter: MainFragmentPresenter, item: FavouriteItem) {
+ this.presenter = presenter
+ this.displayedItem = item
+ }
+
+ private fun initEditDialog() {
+ this.editDialog = Dialog(itemView.context)
+ editDialog.setContentView(R.layout.dialog_edit_or_delete)
+ editDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ val editButton = editDialog.findViewById(R.id.btn_add_or_delete_edit)
+ editButton.setOnClickListener(editOnClickListener)
+ val deleteButton = editDialog.findViewById(R.id.btn_add_or_delete_delete)
+ deleteButton.setOnClickListener(deleteOnClickListener)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt
new file mode 100644
index 0000000..b883c48
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt
@@ -0,0 +1,46 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.formatDateForDisplay
+import app.marcdev.earworm.utils.getArtworkDirectory
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import timber.log.Timber
+
+class MainRecyclerViewHolderAlbum(itemView: View) : MainRecyclerViewHolder(itemView) {
+
+ private val albumNameDisplay: TextView = itemView.findViewById(R.id.txt_albumName)
+ private val albumDateDisplay: TextView = itemView.findViewById(R.id.txt_albumDate)
+ private val albumArtistDisplay: TextView = itemView.findViewById(R.id.txt_albumArtist)
+ private var albumImageDisplay: ImageView = itemView.findViewById(R.id.img_album_icon)
+
+ override fun display(favouriteItemToDisplay: FavouriteItem) {
+ Timber.d("Log: display: $favouriteItemToDisplay")
+ albumNameDisplay.text = favouriteItemToDisplay.albumName
+ albumArtistDisplay.text = favouriteItemToDisplay.artistName
+ val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year)
+ albumDateDisplay.text = date
+
+ if(favouriteItemToDisplay.imageName.isNotBlank()) {
+ Timber.d("Log: display: ${favouriteItemToDisplay.imageName}")
+
+ Glide.with(itemView)
+ .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName)
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(albumImageDisplay)
+ } else {
+ Timber.d("Log: display: No image to display")
+
+ Glide.with(itemView)
+ .load(itemView.resources.getDrawable(R.drawable.ic_album_24px, null))
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(albumImageDisplay)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt
new file mode 100644
index 0000000..d733aee
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt
@@ -0,0 +1,46 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.formatDateForDisplay
+import app.marcdev.earworm.utils.getArtworkDirectory
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import timber.log.Timber
+
+class MainRecyclerViewHolderArtist(itemView: View) : MainRecyclerViewHolder(itemView) {
+
+ private val artistNameDisplay: TextView = itemView.findViewById(R.id.txt_artistName)
+ private val artistGenreDisplay: TextView = itemView.findViewById(R.id.txt_artistGenre)
+ private val artistDateDisplay: TextView = itemView.findViewById(R.id.txt_artistDate)
+ private val artistImageDisplay: ImageView = itemView.findViewById(R.id.img_artist_icon)
+
+ override fun display(favouriteItemToDisplay: FavouriteItem) {
+ Timber.d("Log: display: $favouriteItemToDisplay")
+ artistNameDisplay.text = favouriteItemToDisplay.artistName
+ artistGenreDisplay.text = favouriteItemToDisplay.genre
+ val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year)
+ artistDateDisplay.text = date
+
+ if(favouriteItemToDisplay.imageName.isNotBlank()) {
+ Timber.d("Log: display: imageName = ${favouriteItemToDisplay.imageName}")
+
+ Glide.with(itemView)
+ .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName)
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(artistImageDisplay)
+ } else {
+ Timber.d("Log: display: No image to display")
+
+ Glide.with(itemView)
+ .load(itemView.resources.getDrawable(R.drawable.ic_person_24px, null))
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(artistImageDisplay)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt
new file mode 100644
index 0000000..b51afc8
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt
@@ -0,0 +1,21 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.view.View
+import android.widget.TextView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.formatDateForDisplay
+import timber.log.Timber
+
+class MainRecyclerViewHolderGenre(itemView: View) : MainRecyclerViewHolder(itemView) {
+
+ private var genreNameDisplay: TextView = itemView.findViewById(R.id.txt_genreName)
+ private var genreDateDisplay: TextView = itemView.findViewById(R.id.txt_genreDate)
+
+ override fun display(favouriteItemToDisplay: FavouriteItem) {
+ Timber.d("Log: display: $favouriteItemToDisplay")
+ genreNameDisplay.text = favouriteItemToDisplay.genre
+ val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year)
+ genreDateDisplay.text = date
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt
new file mode 100644
index 0000000..19420dc
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt
@@ -0,0 +1,33 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.view.View
+import android.widget.TextView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.getMonthName
+import timber.log.Timber
+
+open class MainRecyclerViewHolderHeader(itemView: View) : MainRecyclerViewHolder(itemView) {
+
+ private var dateDisplay: TextView = itemView.findViewById(R.id.txt_header_title)
+
+ private val itemClickListener = View.OnClickListener {
+ // Does nothing but overrides default click listener
+ }
+
+ private val itemLongClickListener = View.OnLongClickListener {
+ // Does nothing but overrides default long click listener
+ return@OnLongClickListener true
+ }
+
+ init {
+ itemView.setOnClickListener(itemClickListener)
+ itemView.setOnLongClickListener(itemLongClickListener)
+ }
+
+ override fun display(favouriteItemToDisplay: FavouriteItem) {
+ Timber.d("Log: display: $favouriteItemToDisplay")
+
+ dateDisplay.text = ("${getMonthName(favouriteItemToDisplay.month, itemView.context)} ${favouriteItemToDisplay.year}")
+ }
+}
diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt
new file mode 100644
index 0000000..0611378
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt
@@ -0,0 +1,45 @@
+package app.marcdev.earworm.mainscreen.mainrecycler
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.database.FavouriteItem
+import app.marcdev.earworm.utils.formatDateForDisplay
+import app.marcdev.earworm.utils.getArtworkDirectory
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import timber.log.Timber
+
+class MainRecyclerViewHolderSong(itemView: View) : MainRecyclerViewHolder(itemView) {
+
+ private val songNameDisplay: TextView = itemView.findViewById(R.id.txt_songName)
+ private val songDateDisplay: TextView = itemView.findViewById(R.id.txt_songDate)
+ private val songArtistDisplay: TextView = itemView.findViewById(R.id.txt_songArtist)
+ private val songImageDisplay: ImageView = itemView.findViewById(R.id.img_song_icon)
+
+ override fun display(favouriteItemToDisplay: FavouriteItem) {
+ Timber.d("Log: display: $favouriteItemToDisplay")
+ songNameDisplay.text = favouriteItemToDisplay.songName
+ songArtistDisplay.text = favouriteItemToDisplay.artistName
+ val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year)
+ songDateDisplay.text = date
+
+ if(favouriteItemToDisplay.imageName.isNotBlank()) {
+ Timber.d("Log: display: ${favouriteItemToDisplay.imageName}")
+ Glide.with(itemView)
+ .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName)
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(songImageDisplay)
+ } else {
+ Timber.d("Log: display: No image to display")
+
+ Glide.with(itemView)
+ .load(itemView.resources.getDrawable(R.drawable.ic_music_note_24px, null))
+ .apply(RequestOptions().centerCrop())
+ .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null)))
+ .into(songImageDisplay)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt
new file mode 100644
index 0000000..1b9cebe
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt
@@ -0,0 +1,16 @@
+package app.marcdev.earworm.repository
+
+import app.marcdev.earworm.database.FavouriteItem
+
+interface FavouriteItemRepository {
+
+ suspend fun insertOrUpdateItem(item: FavouriteItem)
+
+ suspend fun getAllItems(): MutableList
+
+ suspend fun getItem(id: Int): MutableList
+
+ suspend fun deleteItem(id: Int)
+
+ suspend fun countUsesOfImage(imageName: String): Int
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt
new file mode 100644
index 0000000..80407c4
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt
@@ -0,0 +1,27 @@
+package app.marcdev.earworm.repository
+
+import app.marcdev.earworm.database.DAO
+import app.marcdev.earworm.database.FavouriteItem
+
+class FavouriteItemRepositoryImpl(private val dao: DAO) : FavouriteItemRepository {
+
+ override suspend fun insertOrUpdateItem(item: FavouriteItem) {
+ return dao.insertOrUpdateItem(item)
+ }
+
+ override suspend fun getAllItems(): MutableList {
+ return dao.getAllItems()
+ }
+
+ override suspend fun getItem(id: Int): MutableList {
+ return dao.getItemById(id)
+ }
+
+ override suspend fun deleteItem(id: Int) {
+ return dao.deleteItemById(id)
+ }
+
+ override suspend fun countUsesOfImage(imageName: String): Int {
+ return dao.getNumberOfEntriesUsingImage(imageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt
new file mode 100644
index 0000000..d910e87
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt
@@ -0,0 +1,114 @@
+package app.marcdev.earworm.settingsscreen
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import app.marcdev.earworm.R
+import app.marcdev.earworm.utils.DARK_THEME
+import app.marcdev.earworm.utils.changeColorOfImageButtonDrawable
+import app.marcdev.earworm.utils.getTheme
+import timber.log.Timber
+
+class LicensesActivity : AppCompatActivity() {
+
+ private var isDarkMode: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Timber.d("Log: onCreate: Started")
+
+ /* Theme changes must be done before super.onCreate otherwise it will be overridden with the value
+ in the manifest */
+ if(getTheme(applicationContext) == DARK_THEME) {
+ Timber.v("Log: onCreate: Is dark mode")
+ setTheme(R.style.Earworm_DarkTheme)
+ isDarkMode = true
+ } else {
+ Timber.v("Log: onCreate: Is not dark mode")
+ setTheme(R.style.Earworm_LightTheme)
+ isDarkMode = false
+ }
+
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_licenses)
+
+ bindViews()
+ }
+
+ private fun bindViews() {
+ Timber.v("Log: bindViews: Started")
+ val backButton = findViewById(R.id.img_backFromSettings)
+ backButton.setOnClickListener(backOnClickListener)
+ if(isDarkMode) {
+ changeColorOfImageButtonDrawable(applicationContext, backButton, false)
+ }
+
+ val toolbarTitle = findViewById(R.id.txt_settingsToolbarTitle)
+ toolbarTitle.text = resources.getString(R.string.licenses)
+
+ val glideCard = findViewById(R.id.card_glide)
+ glideCard.setOnClickListener(glideOnClickListener)
+
+ val timberCard = findViewById(R.id.card_timber)
+ timberCard.setOnClickListener(timberOnClickListener)
+
+ val materialIconsCard = findViewById(R.id.card_material_design_icons)
+ materialIconsCard.setOnClickListener(materialIconsOnClickListener)
+
+ val materialComponentsCard = findViewById(R.id.card_material_design_components)
+ materialComponentsCard.setOnClickListener(materialComponentsOnClickListener)
+
+ val filePickerCard = findViewById(R.id.card_android_file_picker)
+ filePickerCard.setOnClickListener(filePickerOnClickListener)
+
+ }
+
+ private val backOnClickListener = View.OnClickListener {
+ Timber.d("Log: backClick: Started")
+ finish()
+ }
+
+ private val glideOnClickListener = View.OnClickListener {
+ Timber.d("Log: glideClick: Started")
+ val uriUrl = Uri.parse("https://github.com/bumptech/glide")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ }
+
+ private val timberOnClickListener = View.OnClickListener {
+ Timber.d("Log: timberClick: Started")
+ val uriUrl = Uri.parse("https://github.com/JakeWharton/timber")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ }
+
+ private val materialIconsOnClickListener = View.OnClickListener {
+ Timber.d("Log: materialIconsClick: Started")
+ val uriUrl = Uri.parse("https://github.com/google/material-design-icons")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ }
+
+ private val materialComponentsOnClickListener = View.OnClickListener {
+ Timber.d("Log: materialComponentsClick: Started")
+ val uriUrl = Uri.parse("https://github.com/material-components/material-components-android")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ }
+
+ private val filePickerOnClickListener = View.OnClickListener {
+ Timber.d("Log: filePickerClick: Started")
+ val uriUrl = Uri.parse("https://github.com/DroidNinja/Android-FilePicker")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ }
+}
diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt
new file mode 100644
index 0000000..d39ae06
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt
@@ -0,0 +1,49 @@
+package app.marcdev.earworm.settingsscreen
+
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageButton
+import androidx.appcompat.app.AppCompatActivity
+import app.marcdev.earworm.R
+import app.marcdev.earworm.utils.DARK_THEME
+import app.marcdev.earworm.utils.changeColorOfImageButtonDrawable
+import app.marcdev.earworm.utils.getTheme
+import app.marcdev.earworm.utils.setFragment
+import timber.log.Timber
+
+class SettingsActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Timber.v("Log: onCreate: Started")
+
+ if(getTheme(applicationContext) == DARK_THEME) {
+ Timber.v("Log: onCreate: Is dark mode")
+ setTheme(R.style.Earworm_DarkTheme)
+ } else {
+ Timber.v("Log: onCreate: Is not dark mode")
+ setTheme(R.style.Earworm_LightTheme)
+ }
+
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+ bindViews()
+
+ setFragment(SettingsFragment(), supportFragmentManager, R.id.scroll_settings)
+ }
+
+ private fun bindViews() {
+ Timber.v("Log: bindViews: Started")
+ val backButton = findViewById(R.id.img_backFromSettings)
+ backButton.setOnClickListener(backOnClickListener)
+
+ if(getTheme(applicationContext) == DARK_THEME) {
+ Timber.v("Log: bindViews: Converting to dark mode")
+ changeColorOfImageButtonDrawable(applicationContext, backButton, false)
+ }
+ }
+
+ private val backOnClickListener = View.OnClickListener {
+ Timber.d("Log: BackClick: Started")
+ this.finish()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt
new file mode 100644
index 0000000..ecc7014
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt
@@ -0,0 +1,118 @@
+package app.marcdev.earworm.settingsscreen
+
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Bundle
+import android.preference.ListPreference
+import android.preference.PreferenceManager
+import android.widget.Toast
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import app.marcdev.earworm.BuildConfig
+import app.marcdev.earworm.R
+import app.marcdev.earworm.utils.*
+import timber.log.Timber
+
+class SettingsFragment : PreferenceFragmentCompat() {
+
+ private lateinit var prefs: SharedPreferences
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ Timber.v("Log: onCreatePreferences: Started")
+ setPreferencesFromResource(R.xml.preferences, rootKey)
+ this.prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity().applicationContext)
+
+ val themePref = findPreference(PREF_THEME)
+ themePref.onPreferenceChangeListener = themeChangeListener
+ matchSummaryToSelection(themePref, PreferenceManager.getDefaultSharedPreferences(themePref.context).getString(themePref.key, "")!!)
+ changeColorOfDrawable(requireContext(), themePref.icon, false)
+
+ val clearInputsPref = findPreference(PREF_CLEAR_INPUTS)
+ clearInputsPref.onPreferenceChangeListener = clearInputsChangeListener
+ matchSummaryToSelection(clearInputsPref, PreferenceManager.getDefaultSharedPreferences(clearInputsPref.context).getString(clearInputsPref.key, "")!!)
+ changeColorOfDrawable(requireContext(), clearInputsPref.icon, false)
+
+ val tipsPref = findPreference(PREF_SHOW_TIPS)
+ tipsPref.onPreferenceClickListener = resetTipsListener
+ changeColorOfDrawable(requireContext(), tipsPref.icon, false)
+
+ val versionPref = findPreference(PREF_BUILD_NUMBER)
+ versionPref.summary = BuildConfig.VERSION_NAME
+ versionPref.onPreferenceClickListener = versionClickListener
+ changeColorOfDrawable(requireContext(), versionPref.icon, false)
+
+ val licensesPref = findPreference(PREF_LICENSES)
+ licensesPref.onPreferenceClickListener = licensesOnClickListener
+ changeColorOfDrawable(requireContext(), licensesPref.icon, false)
+
+ val githubPref = findPreference(PREF_GITHUB)
+ githubPref.onPreferenceClickListener = githubOnClickListener
+ changeColorOfDrawable(requireContext(), githubPref.icon, false)
+ }
+
+ private val themeChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
+ Timber.d("Log: themeChangeListener: Theme changed to $newValue")
+ requireActivity().recreate()
+ matchSummaryToSelection(preference, newValue.toString())
+ true
+ }
+
+ private val clearInputsChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
+ Timber.d("Log: clearInputsChangeListener: Value changed to $newValue")
+ matchSummaryToSelection(preference, newValue.toString())
+ true
+ }
+
+ private val resetTipsListener = Preference.OnPreferenceClickListener {
+ Timber.d("Log: ResetTipsClick: Clicked")
+ prefs.edit().putBoolean(PREF_SHOW_TIPS, true).apply()
+ Toast.makeText(requireContext(), resources.getString(R.string.reset_tips_confirmation), Toast.LENGTH_LONG).show()
+ return@OnPreferenceClickListener true
+ }
+
+ private val versionClickListener = Preference.OnPreferenceClickListener {
+ Timber.d("Log: versionClick: Started")
+ val versionCodeString = resources.getString(R.string.build_code)
+ Toast.makeText(requireContext(), "$versionCodeString: ${BuildConfig.VERSION_CODE}", Toast.LENGTH_SHORT).show()
+ true
+ }
+
+ private val licensesOnClickListener = Preference.OnPreferenceClickListener {
+ Timber.d("Log: licensesClick: Started")
+ val intent = Intent(requireContext(), LicensesActivity::class.java)
+ startActivity(intent)
+ true
+ }
+
+ private val githubOnClickListener = Preference.OnPreferenceClickListener {
+ Timber.d("Log: githubClick: Started")
+ val uriUrl = Uri.parse("https://github.com/MarcDonald/Earworm")
+ val launchBrowser = Intent(Intent.ACTION_VIEW)
+ launchBrowser.data = uriUrl
+ startActivity(launchBrowser)
+ true
+ }
+
+ private fun matchSummaryToSelection(preference: Preference, value: String) {
+ Timber.d("Log: themeOnChangeListener: Started")
+ Timber.d("Log: themeOnChangeListener: Value = $value")
+
+ if(preference is ListPreference) {
+ val index = preference.findIndexOfValue(value)
+
+ preference.setSummary(
+ if(index >= 0) {
+ Timber.d("Log: BindPreferenceSummaryToValue: Setting summary to ${preference.entries[index]}")
+ preference.entries[index]
+ } else {
+ Timber.w("Log: BindPreferenceSummaryToValue: Index < 0")
+ null
+ })
+
+ } else {
+ Timber.d("Log: BindPreferenceSummaryToValue: Setting summary to $value")
+ preference.summary = value
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt
new file mode 100644
index 0000000..f1c2394
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt
@@ -0,0 +1,163 @@
+package app.marcdev.earworm.uicomponents
+
+import android.app.Dialog
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.DatePicker
+import app.marcdev.earworm.R
+import app.marcdev.earworm.mainscreen.MainFragmentPresenter
+import app.marcdev.earworm.utils.DEFAULT_FILTER
+import app.marcdev.earworm.utils.ItemFilter
+import app.marcdev.earworm.utils.formatDateForDisplay
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.chip.Chip
+import timber.log.Timber
+import java.util.*
+
+class FilterDialog(context: Context, private val presenter: MainFragmentPresenter) : Dialog(context) {
+
+ private lateinit var displaySongCheckbox: CheckBox
+ private lateinit var displayAlbumCheckbox: CheckBox
+ private lateinit var displayArtistCheckbox: CheckBox
+ private lateinit var startDateDisplay: Chip
+ private lateinit var endDateDisplay: Chip
+ private lateinit var startDatePickerDialog: Dialog
+ private lateinit var endDatePickerDialog: Dialog
+ var activeFilter: ItemFilter = DEFAULT_FILTER.copy()
+
+ init {
+ Timber.d("Log: FilterDialog Init: Started")
+ setContentView(R.layout.dialog_filter)
+ window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ bindViews()
+ initCheckboxes()
+ }
+
+ private fun bindViews() {
+ Timber.d("Log: bindViews: Started")
+
+ initStartDatePickerDialog()
+ initEndDatePickerDialog()
+
+ this.startDateDisplay = findViewById(R.id.chip_filter_start)
+ startDateDisplay.setOnClickListener {
+ Timber.d("Log: startDateClickListener: Started")
+ startDatePickerDialog.show()
+ }
+
+ this.endDateDisplay = findViewById(R.id.chip_filter_end)
+ endDateDisplay.setOnClickListener {
+ Timber.d("Log: endDateClickListener: Started")
+ endDatePickerDialog.show()
+ }
+
+ this.displaySongCheckbox = findViewById(R.id.chk_filter_song)
+ displaySongCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+ activeFilter.includeSongs = isChecked
+ }
+
+ this.displayAlbumCheckbox = findViewById(R.id.chk_filter_album)
+ displayAlbumCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+ activeFilter.includeAlbums = isChecked
+ }
+
+ this.displayArtistCheckbox = findViewById(R.id.chk_filter_artist)
+ displayArtistCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+ activeFilter.includeArtists = isChecked
+ }
+
+ val submitButton: MaterialButton = findViewById(R.id.btn_filter_ok)
+ submitButton.setOnClickListener {
+ Timber.d("Log: submitButtonOnClickListener: Started")
+ presenter.getAllItems(activeFilter)
+ dismiss()
+ }
+ }
+
+ private fun initStartDatePickerDialog() {
+ Timber.d("Log: initStartDatePickerDialog: Started")
+
+ this.startDatePickerDialog = Dialog(context)
+ startDatePickerDialog.setContentView(R.layout.dialog_datepicker_filter)
+ startDatePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ val datePicker: DatePicker = startDatePickerDialog.findViewById(R.id.datepicker_filter)
+
+ val cancelButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_cancel)
+ cancelButton.setOnClickListener {
+ Timber.d("Log: cancelButtonOnClickListener: Started")
+ startDatePickerDialog.dismiss()
+ }
+
+ val okButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_ok)
+ okButton.setOnClickListener {
+ Timber.d("Log: okButtonOnClickListener: Started")
+ activeFilter.startDay = datePicker.dayOfMonth
+ activeFilter.startMonth = datePicker.month
+ activeFilter.startYear = datePicker.year
+ startDateDisplay.text = formatDateForDisplay(datePicker.dayOfMonth, datePicker.month, datePicker.year)
+ startDatePickerDialog.dismiss()
+ }
+
+ val startButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_start_end)
+ startButton.setOnClickListener {
+ Timber.d("Log: startButtonOnClickListener: Started")
+ activeFilter.startDay = DEFAULT_FILTER.startDay
+ activeFilter.startMonth = DEFAULT_FILTER.startMonth
+ activeFilter.startYear = DEFAULT_FILTER.startYear
+ startDateDisplay.text = context.resources.getString(R.string.start)
+ val todayCalendar = Calendar.getInstance()
+ datePicker.updateDate(todayCalendar.get(Calendar.YEAR), todayCalendar.get(Calendar.MONTH), todayCalendar.get(Calendar.DAY_OF_MONTH))
+ startDatePickerDialog.dismiss()
+ }
+ }
+
+ private fun initEndDatePickerDialog() {
+ Timber.d("Log: initEndDatePickerDialog: Started")
+
+ this.endDatePickerDialog = Dialog(context)
+ endDatePickerDialog.setContentView(R.layout.dialog_datepicker_filter)
+ endDatePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ val datePicker: DatePicker = endDatePickerDialog.findViewById(R.id.datepicker_filter)
+
+ val cancelButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_cancel)
+ cancelButton.setOnClickListener {
+ Timber.d("Log: cancelButtonOnClickListener: Started")
+ endDatePickerDialog.dismiss()
+ }
+
+ val okButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_ok)
+ okButton.setOnClickListener {
+ Timber.d("Log: okButtonOnClickListener: Started")
+ activeFilter.endDay = datePicker.dayOfMonth
+ activeFilter.endMonth = datePicker.month
+ activeFilter.endYear = datePicker.year
+ endDateDisplay.text = formatDateForDisplay(datePicker.dayOfMonth, datePicker.month, datePicker.year)
+ endDatePickerDialog.dismiss()
+ }
+
+ val endButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_start_end)
+ endButton.text = context.resources.getString(R.string.end)
+ endButton.setOnClickListener {
+ Timber.d("Log: startButtonOnClickListener: Started")
+ activeFilter.endDay = DEFAULT_FILTER.endDay
+ activeFilter.endMonth = DEFAULT_FILTER.endMonth
+ activeFilter.endYear = DEFAULT_FILTER.endYear
+ endDateDisplay.text = context.resources.getString(R.string.end)
+ val todayCalendar = Calendar.getInstance()
+ datePicker.updateDate(todayCalendar.get(Calendar.YEAR), todayCalendar.get(Calendar.MONTH), todayCalendar.get(Calendar.DAY_OF_MONTH))
+ endDatePickerDialog.dismiss()
+ }
+ }
+
+ private fun initCheckboxes() {
+ Timber.d("Log: initCheckboxes: Started")
+ displaySongCheckbox.isChecked = activeFilter.includeSongs
+ displayAlbumCheckbox.isChecked = activeFilter.includeAlbums
+ displayArtistCheckbox.isChecked = activeFilter.includeArtists
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt
new file mode 100644
index 0000000..f8eb9c1
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt
@@ -0,0 +1,18 @@
+package app.marcdev.earworm.uicomponents
+
+import android.app.Dialog
+import android.os.Bundle
+import app.marcdev.earworm.R
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+open class RoundedBottomDialogFragment : BottomSheetDialogFragment() {
+
+ override fun getTheme(): Int {
+ return R.style.Earworm_BottomSheetDialogTheme
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return BottomSheetDialog(requireContext(), theme)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt
new file mode 100644
index 0000000..9078c1f
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt
@@ -0,0 +1,128 @@
+package app.marcdev.earworm.utils
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.preference.PreferenceManager
+import android.widget.ImageButton
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import app.marcdev.earworm.R
+import timber.log.Timber
+
+// Song types
+const val SONG = 0
+const val ALBUM = 1
+const val ARTIST = 2
+const val GENRE = 3
+const val HEADER = 4
+
+val DEFAULT_FILTER = ItemFilter(1, 0, 1900, 31, 11, 2099, true, true, true, "")
+
+// Theme IDs
+const val LIGHT_THEME = 0
+const val DARK_THEME = 1
+
+// Preference keys
+const val PREF_THEME = "pref_theme"
+const val PREF_SHOW_TIPS = "pref_show_tips"
+const val PREF_BUILD_NUMBER = "pref_build_number"
+const val PREF_LICENSES = "pref_licenses"
+const val PREF_GITHUB = "pref_github"
+const val PREF_CLEAR_INPUTS = "pref_clear_inputs_on_type_change"
+
+/**
+ * Replaces a fragment in a frame with another fragment
+ * @param fragment The fragment to display
+ * @param fragmentManager The Fragment Manager
+ * @param frameId The ID of the frame to display the new fragment in
+ */
+fun setFragment(fragment: Fragment, fragmentManager: FragmentManager, frameId: Int) {
+ Timber.d("Log: setFragment: Replacing frame $frameId with fragment $fragment")
+ val fragmentTransaction = fragmentManager.beginTransaction()
+ fragmentTransaction.replace(frameId, fragment)
+ fragmentTransaction.commit()
+}
+
+/**
+ * Converts date to a format suitable for display
+ * @param day The day
+ * @param month The month (indexed at 0 the same as the Java calendar, so January is 0)
+ * @param year The year
+ */
+fun formatDateForDisplay(day: Int, month: Int, year: Int): String {
+ Timber.d("Log: formatDateForDisplay: Started with day = $day, month = $month, year = $year")
+ // Add 1 to month to make it non-zero indexed (January will now be 1 rather than 0)
+ return "$day/${month + 1}/$year"
+}
+
+/**
+ * Changes the color of a drawable in an ImageView to indicate whether it is activated or not.
+ * Deactivated will change the color to either black or 70% white depending on the theme
+ * @param context Context
+ * @param button The button to change the color of
+ * @param isActivated Whether or not the button should be put into the activated state
+ */
+fun changeColorOfImageButtonDrawable(context: Context, button: ImageButton, isActivated: Boolean) {
+ Timber.v("Log: changeColorOfImageButtonDrawable: Started")
+
+ when {
+ isActivated -> button.setColorFilter(context.getColor(R.color.colorAccent))
+ (getTheme(context) == DARK_THEME && !isActivated) -> button.setColorFilter(context.getColor(R.color.white60))
+ else -> button.setColorFilter(context.getColor(R.color.black))
+ }
+}
+
+/**
+ * Changes the color of a drawable to indicate whether it is activated or not. Deactivated will
+ * change the color to either black or 70% white depending on the theme
+ * @param context Context
+ * @param drawable The drawable to change the color of
+ * @param isActivated Whether or not the button should be put into the activated state
+ */
+fun changeColorOfDrawable(context: Context, drawable: Drawable, isActivated: Boolean) {
+ Timber.v("Log: changeColorOfDrawable: Started")
+
+ when {
+ isActivated -> drawable.setColorFilter(context.getColor(R.color.colorAccent), PorterDuff.Mode.SRC_IN)
+ (getTheme(context) == DARK_THEME && !isActivated) -> drawable.setColorFilter((context.getColor(R.color.white60)), PorterDuff.Mode.SRC_IN)
+ else -> drawable.setColorFilter((context.getColor(R.color.black)), PorterDuff.Mode.SRC_IN)
+ }
+}
+
+/**
+ * Gets the full name of a month based on it's 0 indexed number
+ * @param month Integer value of the month, indexed at 0
+ * @param context Context
+ * @return Full name of the month in string format
+ */
+fun getMonthName(month: Int, context: Context): String {
+ val monthArray = context.resources.getStringArray(R.array.months)
+ return monthArray[month]
+}
+
+/**
+ * Gets the path of the application's image storage
+ * @param context Context
+ */
+fun getArtworkDirectory(context: Context): String {
+ Timber.d("Log: getArtworkDirectory: Started")
+ val returnValue = context.filesDir.path + "/artwork/"
+ Timber.d("Log: getArtworkDirectory: Returning $returnValue")
+ return returnValue
+}
+
+/**
+ * Checks the shared preferences to see if the user has selected dark mode
+ * @param context Context
+ */
+fun getTheme(context: Context): Int {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ val theme = prefs.getString("pref_theme", context.resources.getString(R.string.light))
+
+ return when(theme) {
+ context.resources.getString(R.string.light) -> LIGHT_THEME
+ context.resources.getString(R.string.dark) -> DARK_THEME
+ else -> -1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/utils/ItemFilter.kt b/app/src/main/java/app/marcdev/earworm/utils/ItemFilter.kt
new file mode 100644
index 0000000..efd7aa3
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/utils/ItemFilter.kt
@@ -0,0 +1,12 @@
+package app.marcdev.earworm.utils
+
+data class ItemFilter(var startDay: Int,
+ var startMonth: Int,
+ var startYear: Int,
+ var endDay: Int,
+ var endMonth: Int,
+ var endYear: Int,
+ var includeSongs: Boolean,
+ var includeAlbums: Boolean,
+ var includeArtists: Boolean,
+ var searchTerm: String)
\ No newline at end of file
diff --git a/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt
new file mode 100644
index 0000000..563e2d4
--- /dev/null
+++ b/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt
@@ -0,0 +1,140 @@
+package app.marcdev.earworm.utils
+
+import app.marcdev.earworm.database.FavouriteItem
+import timber.log.Timber
+
+/**
+ * Filters a list based on a filter input by the user
+ * @param allItems The complete list of items from the database
+ * @param filter The filter to apply
+ * @return Filtered list
+ */
+fun applyFilter(allItems: MutableList, filter: ItemFilter): MutableList {
+ val filteredItems = mutableListOf()
+ filteredItems.addAll(allItems)
+
+ val startDayTwoDigit = if(filter.startDay < 10)
+ "0${filter.startDay}"
+ else {
+ "${filter.startDay}"
+ }
+
+ val startMonthTwoDigit = if(filter.startMonth < 10)
+ "0${filter.startMonth}"
+ else {
+ "${filter.startMonth}"
+ }
+
+ val filterCompleteDateStart: Int = "${filter.startYear}$startMonthTwoDigit$startDayTwoDigit".toInt()
+
+ val endDayTwoDigit = if(filter.endDay < 10)
+ "0${filter.endDay}"
+ else {
+ "${filter.endDay}"
+ }
+
+ val endMonthTwoDigit = if(filter.endMonth < 10)
+ "0${filter.endMonth}"
+ else {
+ "${filter.endMonth}"
+ }
+
+ val filterCompleteDateEnd: Int = "${filter.endYear}$endMonthTwoDigit$endDayTwoDigit".toInt()
+
+ for(x in 0 until allItems.size) {
+ if(allItems[x].type == SONG && !filter.includeSongs) {
+ filteredItems.remove(allItems[x])
+ }
+
+ if(allItems[x].type == ALBUM && !filter.includeAlbums) {
+ filteredItems.remove(allItems[x])
+ }
+
+ if(allItems[x].type == ARTIST && !filter.includeArtists) {
+ filteredItems.remove(allItems[x])
+ }
+
+ val dayTwoDigit = if(allItems[x].day < 10)
+ "0${allItems[x].day}"
+ else {
+ "${allItems[x].day}"
+ }
+
+ val monthTwoDigit = if(allItems[x].month < 10)
+ "0${allItems[x].month}"
+ else {
+ "${allItems[x].month}"
+ }
+
+ val completeDate = "${allItems[x].year}$monthTwoDigit$dayTwoDigit"
+ val completeDateI: Int = completeDate.toInt()
+
+ if(completeDateI < filterCompleteDateStart || completeDateI > filterCompleteDateEnd) {
+ filteredItems.remove(allItems[x])
+ }
+
+ if(!(allItems[x].albumName.contains(filter.searchTerm, true))
+ && !(allItems[x].artistName.contains(filter.searchTerm, true))
+ && !(allItems[x].songName.contains(filter.searchTerm, true))
+ && !(allItems[x].genre.contains(filter.searchTerm, true))
+ ) {
+ filteredItems.remove(allItems[x])
+ }
+ }
+
+ return sortByDateDescending(filteredItems)
+}
+
+/**
+ * Sorts a list by date in descending order
+ * @param items List to sort
+ * @return Sorted list of FavouriteItems
+ */
+fun sortByDateDescending(items: MutableList): MutableList {
+ val filteredItems = items.sortedWith(
+ compareBy(
+ { -it.year },
+ { -it.month },
+ { -it.day },
+ { -it.id!! }))
+
+ return filteredItems.toMutableList()
+}
+
+/**
+ * Adds header items to a list on a monthly basis
+ * @param allItems List to add headers to (sorted by date descending)
+ * @return List with headers every new month
+ */
+fun addListHeaders(allItems: MutableList): List {
+ val listWithHeaders = mutableListOf()
+ listWithHeaders.addAll(allItems)
+
+ var lastMonth = 12
+ var lastYear = 9999
+ if(allItems.isNotEmpty()) {
+ lastMonth = allItems.first().month + 1
+ lastYear = allItems.first().year
+ }
+
+ val headersToAdd = mutableListOf>()
+
+ for(x in 0 until allItems.size) {
+ if(((allItems[x].month < lastMonth) && (allItems[x].year == lastYear))
+ || (allItems[x].month > lastMonth) && (allItems[x].year < lastYear)
+ || (allItems[x].year < lastYear)
+ ) {
+ Timber.v("Log: addListHeaders: x = $x")
+ val header = FavouriteItem("", "", "", "", 0, allItems[x].month, allItems[x].year, HEADER, "")
+ lastMonth = allItems[x].month
+ lastYear = allItems[x].year
+ headersToAdd.add(Pair(x, header))
+ }
+ }
+
+ for((add, x) in (0 until headersToAdd.size).withIndex()) {
+ listWithHeaders.add(headersToAdd[x].first + add, headersToAdd[x].second)
+ }
+
+ return listWithHeaders
+}
diff --git a/app/src/main/res/drawable/ic_add_24px.xml b/app/src/main/res/drawable/ic_add_24px.xml
new file mode 100644
index 0000000..d70af2a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_24px.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_add_a_photo_24px.xml b/app/src/main/res/drawable/ic_add_a_photo_24px.xml
new file mode 100644
index 0000000..1323196
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_a_photo_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_add_circle_24px.xml b/app/src/main/res/drawable/ic_add_circle_24px.xml
new file mode 100644
index 0000000..ba908ba
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_circle_24px.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_album_24px.xml b/app/src/main/res/drawable/ic_album_24px.xml
new file mode 100644
index 0000000..279ad3a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_album_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_back_24px.xml b/app/src/main/res/drawable/ic_arrow_back_24px.xml
new file mode 100644
index 0000000..8f329da
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_back_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_backspace_black_24dp.xml b/app/src/main/res/drawable/ic_backspace_black_24dp.xml
new file mode 100644
index 0000000..3659b08
--- /dev/null
+++ b/app/src/main/res/drawable/ic_backspace_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_build_black_24dp.xml b/app/src/main/res/drawable/ic_build_black_24dp.xml
new file mode 100644
index 0000000..255931f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_build_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close_24px.xml b/app/src/main/res/drawable/ic_close_24px.xml
new file mode 100644
index 0000000..dd91559
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_code_black_24dp.xml b/app/src/main/res/drawable/ic_code_black_24dp.xml
new file mode 100644
index 0000000..c6b6a06
--- /dev/null
+++ b/app/src/main/res/drawable/ic_code_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_color_lens_black_24dp.xml b/app/src/main/res/drawable/ic_color_lens_black_24dp.xml
new file mode 100644
index 0000000..f79e30b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_color_lens_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_description_black_24dp.xml b/app/src/main/res/drawable/ic_description_black_24dp.xml
new file mode 100644
index 0000000..bef4f34
--- /dev/null
+++ b/app/src/main/res/drawable/ic_description_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_error_24px.xml b/app/src/main/res/drawable/ic_error_24px.xml
new file mode 100644
index 0000000..be7674a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter_list_24px.xml b/app/src/main/res/drawable/ic_filter_list_24px.xml
new file mode 100644
index 0000000..0941934
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter_list_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_black_24dp.xml b/app/src/main/res/drawable/ic_help_black_24dp.xml
new file mode 100644
index 0000000..b1d2812
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_library_music_black_24dp.xml b/app/src/main/res/drawable/ic_library_music_black_24dp.xml
new file mode 100644
index 0000000..4881e45
--- /dev/null
+++ b/app/src/main/res/drawable/ic_library_music_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_music_note_24px.xml b/app/src/main/res/drawable/ic_music_note_24px.xml
new file mode 100644
index 0000000..1e268ef
--- /dev/null
+++ b/app/src/main/res/drawable/ic_music_note_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_person_24px.xml b/app/src/main/res/drawable/ic_person_24px.xml
new file mode 100644
index 0000000..153c4e8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_person_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_search_24px.xml b/app/src/main/res/drawable/ic_search_24px.xml
new file mode 100644
index 0000000..4e63214
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sentiment_dissatisfied_black_24dp.xml b/app/src/main/res/drawable/ic_sentiment_dissatisfied_black_24dp.xml
new file mode 100644
index 0000000..2e57e93
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sentiment_dissatisfied_black_24dp.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_settings_24px.xml b/app/src/main/res/drawable/ic_settings_24px.xml
new file mode 100644
index 0000000..1de8ad9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bottom_sheet_background.xml b/app/src/main/res/drawable/rounded_bottom_sheet_background.xml
new file mode 100644
index 0000000..8a6ae6d
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bottom_sheet_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_datepicker_background.xml b/app/src/main/res/drawable/rounded_datepicker_background.xml
new file mode 100644
index 0000000..519d96a
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_datepicker_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_dialog_background.xml b/app/src/main/res/drawable/rounded_dialog_background.xml
new file mode 100644
index 0000000..1f1014e
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_dialog_background.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_licenses.xml b/app/src/main/res/layout/activity_licenses.xml
new file mode 100644
index 0000000..f828643
--- /dev/null
+++ b/app/src/main/res/layout/activity_licenses.xml
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..42bf02c
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..5e083d2
--- /dev/null
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_add_item.xml b/app/src/main/res/layout/dialog_add_item.xml
new file mode 100644
index 0000000..5c8608a
--- /dev/null
+++ b/app/src/main/res/layout/dialog_add_item.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_datepicker.xml b/app/src/main/res/layout/dialog_datepicker.xml
new file mode 100644
index 0000000..b91904d
--- /dev/null
+++ b/app/src/main/res/layout/dialog_datepicker.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_datepicker_filter.xml b/app/src/main/res/layout/dialog_datepicker_filter.xml
new file mode 100644
index 0000000..dfe70bd
--- /dev/null
+++ b/app/src/main/res/layout/dialog_datepicker_filter.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_delete_image.xml b/app/src/main/res/layout/dialog_delete_image.xml
new file mode 100644
index 0000000..9dbdfe9
--- /dev/null
+++ b/app/src/main/res/layout/dialog_delete_image.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_edit_or_delete.xml b/app/src/main/res/layout/dialog_edit_or_delete.xml
new file mode 100644
index 0000000..47ec010
--- /dev/null
+++ b/app/src/main/res/layout/dialog_edit_or_delete.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml
new file mode 100644
index 0000000..cd957db
--- /dev/null
+++ b/app/src/main/res/layout/dialog_filter.xml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_mainscreen.xml b/app/src/main/res/layout/fragment_mainscreen.xml
new file mode 100644
index 0000000..b055482
--- /dev/null
+++ b/app/src/main/res/layout/fragment_mainscreen.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml
new file mode 100644
index 0000000..dd26bae
--- /dev/null
+++ b/app/src/main/res/layout/item_header.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_mainrecycler_album.xml b/app/src/main/res/layout/item_mainrecycler_album.xml
new file mode 100644
index 0000000..cbe666c
--- /dev/null
+++ b/app/src/main/res/layout/item_mainrecycler_album.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_mainrecycler_artist.xml b/app/src/main/res/layout/item_mainrecycler_artist.xml
new file mode 100644
index 0000000..688b80e
--- /dev/null
+++ b/app/src/main/res/layout/item_mainrecycler_artist.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_mainrecycler_genre.xml b/app/src/main/res/layout/item_mainrecycler_genre.xml
new file mode 100644
index 0000000..7636b28
--- /dev/null
+++ b/app/src/main/res/layout/item_mainrecycler_genre.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_mainrecycler_song.xml b/app/src/main/res/layout/item_mainrecycler_song.xml
new file mode 100644
index 0000000..3264a43
--- /dev/null
+++ b/app/src/main/res/layout/item_mainrecycler_song.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_filter.xml b/app/src/main/res/layout/toolbar_filter.xml
new file mode 100644
index 0000000..8900531
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_filter.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_main.xml b/app/src/main/res/layout/toolbar_main.xml
new file mode 100644
index 0000000..c776ea8
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_main.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_settings.xml b/app/src/main/res/layout/toolbar_settings.xml
new file mode 100644
index 0000000..eb00be5
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_settings.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c9ad5f9
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..2b7f809
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..513d2b2
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c15627b
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..513d2b2
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..1e38d4d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..1a6a425
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1e38d4d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..70204be
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2d2e7ce
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..70204be
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..2fb167e
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..a9b0b52
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2fb167e
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..023376d
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..94e39df
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..023376d
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..38ecd6e
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..56922ff
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,36 @@
+
+
+ @color/white
+ #CCCCCC
+ #F57C00
+ #FFAD42
+ #BB4D00
+ #B00020
+ #4CAF50
+
+
+ @color/white
+ @color/white
+ #CCCCCC
+ @color/black
+ @color/black70
+ @color/black50
+
+
+ #303030
+ #212121
+ #424242
+ @color/white
+ @color/white70
+ @color/white50
+
+
+
+ #FFFFFF
+ #B3FFFFFF
+ #99FFFFFF
+ #80FFFFFF
+ #000000
+ #B3000000
+ #80000000
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..f71d525
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+
+
+ 8dp
+ 4dp
+ 16dp
+ 56dp
+ 48dp
+ 16dp
+ 8dp
+ 16dp
+ 8dp
+ 8dp
+ 8dp
+ 90dp
+ 56dp
+ 150dp
+ 40dp
+ 32dp
+ 16dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..16cdd5a
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0b1a775
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,102 @@
+
+
+ Earworm
+ You haven\'t made any entries yet. To add a new entry, click the button in the bottom
+ right
+
+ No results found that match that filter
+ Please ensure all fields are filled before saving
+ Please enter something to search for
+ Today
+ Hold to edit or delete
+ Don\'t Show Again
+ Search
+ Filter
+ Between
+ Include
+ Start Date
+ End Date
+ End
+ Start
+ An Error Occurred
+ Back
+ Add
+ Add an Entry
+ Shortcut Disabled
+ Yes
+ No
+
+
+ Song name
+ Song
+ Album
+ Artist
+ Genre
+ Date
+
+
+ Item Added
+ Item Deleted
+ Save
+ OK
+ Cancel
+ Edit or Delete Item
+ Edit
+ Delete
+ Are you sure you want to remove this image?
+ Add an Image
+ Choose an Image
+
+
+ Settings
+ Appearance
+ Theme Selection
+ Behaviour
+ Reset Tips
+ Clear Inputs on Type Change
+ Tips have been reset
+ App Info
+ Version
+ Build Code
+ Licenses
+ Github
+
+
+ - @string/yes
+ - @string/no
+
+
+
+ Glide
+ Timber
+ Material Design Icons
+ Material Components for Android
+ Android File picker
+ Apache 2
+ BSD, part MIT and Apache 2.0. See the LICENSE file for details
+
+
+ Light
+ Dark
+
+
+ - @string/light
+ - @string/dark
+
+
+
+ - January
+ - February
+ - March
+ - April
+ - May
+ - June
+ - July
+ - August
+ - September
+ - October
+ - November
+ - December
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ab964e1
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..1a16dcf
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 0000000..0b35516
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..ff630b8
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.0'
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.2.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3d8ce0c
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9a4163a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'