├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── Jenkinsfile ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── stylingandroid │ │ │ └── muselee │ │ │ ├── MuseleeActivity.kt │ │ │ ├── MuseleeAppGlideModule.kt │ │ │ ├── MuseleeApplication.kt │ │ │ └── di │ │ │ ├── ActivityModule.kt │ │ │ ├── ApplicationComponent.kt │ │ │ └── ApplicationModule.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── stylingandroid │ └── muselee │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Dependencies.kt ├── cloudbuild.yaml ├── core ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── core │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── stylingandroid │ │ │ └── muselee │ │ │ ├── connectivity │ │ │ ├── ConnectivityLiveData.kt │ │ │ ├── ConnectivityMonitor.kt │ │ │ └── ConnectivityState.kt │ │ │ ├── di │ │ │ ├── BaseViewModule.kt │ │ │ ├── CoreNetworkModule.kt │ │ │ ├── ViewModelKey.kt │ │ │ └── WorkerKey.kt │ │ │ ├── providers │ │ │ ├── DataMapper.kt │ │ │ ├── DataPersister.kt │ │ │ ├── DataProvider.kt │ │ │ └── UpdateScheduler.kt │ │ │ ├── view │ │ │ └── ViewModelFactory.kt │ │ │ └── work │ │ │ └── DaggerWorkerFactory.kt │ └── res │ │ ├── font │ │ ├── muli_extrabold.xml │ │ └── muli_semibold.xml │ │ └── values │ │ ├── font_certs.xml │ │ ├── preloaded_fonts.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── stylingandroid │ └── muselee │ └── core │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── topartists ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── stylingandroid │ └── muselee │ └── topartists │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── topartists │ │ ├── database │ │ ├── DatabaseTopArtistsMapper.kt │ │ ├── DatabaseTopArtistsPersister.kt │ │ ├── DbArtist.kt │ │ ├── TopArtistsDao.kt │ │ └── TopArtistsDatabase.kt │ │ ├── di │ │ ├── DatabaseModule.kt │ │ ├── EntitiesModule.kt │ │ ├── LastFmTopArtistsModule.kt │ │ ├── NetworkModule.kt │ │ ├── SchedulerModule.kt │ │ └── TopArtistsModule.kt │ │ ├── entities │ │ ├── Artist.kt │ │ ├── TopArtistsRepository.kt │ │ └── TopArtistsState.kt │ │ ├── net │ │ ├── LastFm.kt │ │ ├── LastFmArtistsMapper.kt │ │ ├── LastFmTopArtistsApi.kt │ │ └── LastFmTopArtistsProvider.kt │ │ ├── scheduler │ │ ├── TopArtistsScheduler.kt │ │ └── TopArtistsUpdateWorker.kt │ │ └── view │ │ ├── GridPositionCalculator.kt │ │ ├── TopArtistsAdapter.kt │ │ ├── TopArtistsDiffUtil.kt │ │ ├── TopArtistsFragment.kt │ │ ├── TopArtistsItemDecoraton.kt │ │ ├── TopArtistsViewHolder.kt │ │ ├── TopArtistsViewModel.kt │ │ ├── TopArtistsViewState.kt │ │ └── ViewSize.kt └── res │ ├── layout-land │ ├── item_chart_artist_full.xml │ ├── item_chart_artist_medium.xml │ └── item_chart_artist_small.xml │ ├── layout │ ├── fragment_top_artists.xml │ ├── item_chart_artist_full.xml │ ├── item_chart_artist_medium.xml │ └── item_chart_artist_small.xml │ └── values │ ├── dimens.xml │ └── strings.xml └── test └── java └── com └── stylingandroid └── muselee └── topartists └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /captures 12 | .externalNativeBuild 13 | **/build 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 222 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Android 21 | 22 | 23 | Class structureJava 24 | 25 | 26 | Cloning issuesJava 27 | 28 | 29 | Code style issuesJava 30 | 31 | 32 | CorrectnessLintAndroid 33 | 34 | 35 | Groovy 36 | 37 | 38 | Inheritance issuesJava 39 | 40 | 41 | Internationalization issuesJava 42 | 43 | 44 | Java 45 | 46 | 47 | LintAndroid 48 | 49 | 50 | Numeric issuesJava 51 | 52 | 53 | Performance issuesJava 54 | 55 | 56 | PerformanceLintAndroid 57 | 58 | 59 | Potentially confusing code constructsGroovy 60 | 61 | 62 | Probable bugsJava 63 | 64 | 65 | Security issuesJava 66 | 67 | 68 | Serialization issuesJava 69 | 70 | 71 | Threading issuesJava 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | stage('Checkout') { 3 | checkout scm 4 | } 5 | 6 | stage('Clean') { 7 | sh "./gradlew clean" 8 | } 9 | 10 | stage('Build') { 11 | sh "./gradlew clean assemble" 12 | } 13 | 14 | stage('Test') { 15 | sh "./gradlew test" 16 | } 17 | 18 | stage('Check') { 19 | sh "./gradlew check" 20 | } 21 | 22 | stage('Detekt') { 23 | sh "./gradlew detekt" 24 | } 25 | 26 | stage('Report') { 27 | androidLint canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '**/lint-results.xml', unHealthy: '', unstableTotalAll: '0' 28 | step([$class: 'CheckStylePublisher', canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '**/reports/**/detekt.xml', unHealthy: '', unstableTotalAll: '0']) 29 | junit '**/test-results/*Debug*/*.xml' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "{}" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Mark Allison 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Muselee 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.androidApplication) 3 | id(BuildPlugins.kotlinAndroid) 4 | id(BuildPlugins.kotlinAndroidExtensions) 5 | id(BuildPlugins.kotlinKapt) 6 | id(BuildPlugins.detekt) version (BuildPlugins.Versions.detekt) 7 | } 8 | 9 | android { 10 | compileSdkVersion(AndroidSdk.compile) 11 | defaultConfig { 12 | applicationId = "com.stylingandroid.muselee" 13 | minSdkVersion(AndroidSdk.min) 14 | targetSdkVersion(AndroidSdk.target) 15 | versionCode = 1 16 | versionName = "1.0" 17 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | lintOptions { 26 | disable("IconLauncherShape") 27 | } 28 | compileOptions { 29 | targetCompatibility = JavaVersion.VERSION_1_8 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(project(ProjectModules.core)) 36 | implementation(project(ProjectModules.topartists)) 37 | 38 | implementation(Libraries.kotlinStdLib) 39 | implementation(Libraries.appCompat) 40 | implementation(Libraries.ktxCore) 41 | implementation(Libraries.constraintLayout) 42 | implementation(Libraries.dagger) 43 | implementation(Libraries.daggerAndroid) 44 | implementation(Libraries.glide) 45 | kapt(Libraries.daggerCompiler) 46 | kapt(Libraries.daggerAndroidCompiler) 47 | kapt(Libraries.glideCompiler) 48 | 49 | testImplementation(TestLibraries.junit4) 50 | } 51 | 52 | detekt { 53 | version = BuildPlugins.Versions.detekt 54 | input = files("src/main/java", "src/androidx/java", "src/support/java") 55 | filters = ".*test.*,.*/resources/.*,.*/tmp/.*" 56 | } 57 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/stylingandroid/muselee/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.stylingandroid.muselee", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/MuseleeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee 2 | 3 | import android.os.Bundle 4 | import com.stylingandroid.muselee.topartists.view.TopArtistsFragment 5 | import dagger.android.support.DaggerAppCompatActivity 6 | 7 | class MuseleeActivity : DaggerAppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | setContentView(R.layout.activity_main) 13 | supportFragmentManager.beginTransaction().apply { 14 | replace(R.id.main_fragment, TopArtistsFragment()) 15 | commit() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/MuseleeAppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.bumptech.glide.BuildConfig 6 | import com.bumptech.glide.GlideBuilder 7 | import com.bumptech.glide.annotation.GlideModule 8 | import com.bumptech.glide.module.AppGlideModule 9 | 10 | @GlideModule 11 | class MuseleeAppGlideModule : AppGlideModule() { 12 | 13 | override fun applyOptions(context: Context, builder: GlideBuilder) { 14 | super.applyOptions(context, builder) 15 | if (BuildConfig.DEBUG) { 16 | builder.setLogLevel(Log.VERBOSE) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/MuseleeApplication.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee 2 | 3 | import androidx.work.Configuration 4 | import androidx.work.WorkManager 5 | import com.stylingandroid.muselee.di.DaggerApplicationComponent 6 | import com.stylingandroid.muselee.work.DaggerWorkerFactory 7 | import dagger.android.AndroidInjector 8 | import dagger.android.support.DaggerApplication 9 | import javax.inject.Inject 10 | 11 | class MuseleeApplication : DaggerApplication() { 12 | 13 | override fun applicationInjector(): AndroidInjector = 14 | DaggerApplicationComponent.builder().create(this) 15 | 16 | @Inject 17 | lateinit var workerFactory: DaggerWorkerFactory 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | 22 | WorkManager.initialize( 23 | this, 24 | Configuration.Builder() 25 | .setWorkerFactory(workerFactory) 26 | .build() 27 | ) 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/di/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import com.stylingandroid.muselee.MuseleeActivity 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | @Suppress("unused") 9 | abstract class ActivityModule { 10 | 11 | @ContributesAndroidInjector 12 | abstract fun bindMuseleeActivity(): MuseleeActivity 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/di/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import com.stylingandroid.muselee.MuseleeApplication 4 | import com.stylingandroid.muselee.topartists.di.TopArtistsModule 5 | import dagger.Component 6 | import dagger.android.AndroidInjectionModule 7 | import dagger.android.AndroidInjector 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | @Component( 12 | modules = [ 13 | AndroidInjectionModule::class, 14 | ApplicationModule::class, 15 | ActivityModule::class, 16 | TopArtistsModule::class 17 | ] 18 | ) 19 | interface ApplicationComponent : AndroidInjector { 20 | 21 | @Component.Builder 22 | abstract class Builder : AndroidInjector.Builder() 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/stylingandroid/muselee/di/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.net.ConnectivityManager 6 | import com.stylingandroid.muselee.MuseleeApplication 7 | import com.stylingandroid.muselee.connectivity.ConnectivityLiveData 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | object ApplicationModule { 14 | 15 | @Provides 16 | @JvmStatic 17 | @Singleton 18 | internal fun provideApplication(app: MuseleeApplication): Application = app 19 | 20 | @Provides 21 | @JvmStatic 22 | @Singleton 23 | fun providesConnectivityManager(app: Application): ConnectivityManager = 24 | app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 25 | 26 | @Provides 27 | @JvmStatic 28 | @Singleton 29 | fun providesConnectivityLiveData(app: Application) = 30 | ConnectivityLiveData(app) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 17 | 20 | 23 | 24 | 25 | 26 | 29 | 30 | 36 | 39 | 42 | 43 | 44 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1E9618 4 | #4EE147 5 | #156912 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Muselee 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/stylingandroid/muselee/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath(BuildPlugins.androidGradlePlugin) 11 | classpath(BuildPlugins.kotlinGradlePlugin) 12 | } 13 | } 14 | 15 | plugins { 16 | id("com.github.ben-manes.versions") version("0.21.0") 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | } 24 | } 25 | 26 | tasks.register("clean").configure { 27 | delete("build") 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | jcenter() 3 | } 4 | 5 | plugins { 6 | `kotlin-dsl` 7 | } 8 | 9 | kotlinDslPluginOptions { 10 | experimentalWarning.set(false) 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Dependencies.kt: -------------------------------------------------------------------------------- 1 | const val kotlinVersion = "1.3.21" 2 | 3 | object BuildPlugins { 4 | object Versions { 5 | const val androidBuildToolsVersion = "3.5.0-alpha10" 6 | const val detekt = "1.0.0-RC14" 7 | } 8 | 9 | const val androidGradlePlugin = "com.android.tools.build:gradle:${Versions.androidBuildToolsVersion}" 10 | const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 11 | 12 | const val androidApplication = "com.android.application" 13 | const val androidLibrary = "com.android.library" 14 | const val kotlinAndroid = "kotlin-android" 15 | const val kotlinAndroidExtensions = "kotlin-android-extensions" 16 | const val kotlinKapt = "kotlin-kapt" 17 | const val detekt = "io.gitlab.arturbosch.detekt" 18 | } 19 | 20 | object AndroidSdk { 21 | const val min = 21 22 | const val compile = "android-Q" 23 | const val target = "Q" 24 | } 25 | 26 | object ProjectModules { 27 | const val core = ":core" 28 | const val topartists = ":topartists" 29 | } 30 | 31 | object Libraries { 32 | private object Versions { 33 | const val kotlinCoroutines = "1.1.1" 34 | const val jetpack = "1.1.0-alpha02" 35 | const val material = "1.0.0" 36 | const val constraintLayout = "2.0.0-alpha3" 37 | const val archLifecycle = "1.1.1" 38 | const val workManager = "1.0.0" 39 | const val archRoom = "1.1.1" 40 | const val ktx = "1.1.0-alpha04" 41 | const val dagger = "2.21" 42 | const val glide = "4.9.0" 43 | const val okHttp = "3.13.1" 44 | const val retrofit = "2.5.0" 45 | } 46 | 47 | const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 48 | const val kotlinCoroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}" 49 | const val kotlinCoroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinCoroutines}" 50 | const val appCompat = "androidx.appcompat:appcompat:${Versions.jetpack}" 51 | const val materialComponents = "com.google.android.material:material:${Versions.material}" 52 | const val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}" 53 | const val archLifecycle = "android.arch.lifecycle:extensions:${Versions.archLifecycle}" 54 | const val archRoomRuntime = "android.arch.persistence.room:runtime:${Versions.archRoom}" 55 | const val archRoomCompiler = "android.arch.persistence.room:compiler:${Versions.archRoom}" 56 | const val workManager = "android.arch.work:work-runtime-ktx:${Versions.workManager}" 57 | const val ktxCore = "androidx.core:core-ktx:${Versions.ktx}" 58 | const val dagger = "com.google.dagger:dagger:${Versions.dagger}" 59 | const val daggerAndroid = "com.google.dagger:dagger-android-support:${Versions.dagger}" 60 | const val daggerCompiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" 61 | const val daggerAndroidCompiler = "com.google.dagger:dagger-android-processor:${Versions.dagger}" 62 | const val okHttp = "com.squareup.okhttp3:okhttp:${Versions.okHttp}" 63 | const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.okHttp}" 64 | const val glide = "com.github.bumptech.glide:glide:${Versions.glide}" 65 | const val glideCompiler = "com.github.bumptech.glide:compiler:${Versions.glide}" 66 | const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}" 67 | const val retrofitMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}" 68 | } 69 | 70 | object TestLibraries { 71 | private object Versions { 72 | const val junit4 = "4.13-beta-2" 73 | } 74 | const val junit4 = "junit:junit:${Versions.junit4}" 75 | } 76 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | 3 | # 4 | # Extract the cache 5 | # 6 | # The gradle build cache is stored as a tarball in Google Cloud Storage to 7 | # make builds faster. 8 | # 9 | # After extracting the cache to the /build_cache directory, we need to supply 10 | # that to gradle, and include the volume in steps that require the cache. 11 | # 12 | - name: 'gcr.io/cloud-builders/gsutil' 13 | id: copy_build_cache 14 | # we use rsync and not cp so that this step doesn't fail the first time it's run 15 | args: ['rsync', 'gs://${_CACHE_BUCKET}/', '/build_cache'] 16 | volumes: 17 | - name: 'build_cache' 18 | path: '/build_cache' 19 | 20 | # 21 | # Get a build number 22 | # 23 | # Cloud build doesn't use incrementing build numbers, by default, but they're often 24 | # useful for Android builds, so these steps will read/increment/store a build number. 25 | # 26 | # Download the config bucket, which stores the build number. 27 | - name: 'gcr.io/cloud-builders/gsutil' 28 | id: copy_config 29 | waitFor: ['-'] # The '-' indicates that this step begins immediately. 30 | # we use rsync and not cp so that this step doesn't fail the first time it's run 31 | args: ['rsync', 'gs://${_CONFIG_BUCKET}/', '/config'] 32 | volumes: 33 | - name: 'config' 34 | path: '/config' 35 | 36 | # Compound bash command to: 37 | # 1. read a version 38 | # 2. increment it 39 | # 3. write it back to the version file 40 | # 4. write it to the `build_env` file for use later 41 | - name: 'gcr.io/$PROJECT_ID/tar' 42 | id: setup_env 43 | entrypoint: 'bash' 44 | args: 45 | - '-c' 46 | - | 47 | cat /config/buildnum | awk '{print $1"+1"}' | bc | tee /config/buildnum | awk '{print "BUILD_NUM="$1 }' | tee .buildnum 48 | waitFor: ['copy_config'] 49 | volumes: 50 | - name: 'config' 51 | path: '/config' 52 | 53 | - name: 'gcr.io/$PROJECT_ID/tar' 54 | id: extract_build_cache 55 | waitFor: ['copy_build_cache'] 56 | # This might fail the first time, but that's okay 57 | entrypoint: 'bash' 58 | args: 59 | - '-c' 60 | - | 61 | tar xpzf /build_cache/cache.tgz -C / || echo "No cache found." 62 | volumes: 63 | - name: 'build_cache' 64 | path: '/build_cache' 65 | # 66 | # Build the project 67 | # 68 | - name: 'gcr.io/$PROJECT_ID/android:${_ANDROID_VERSION}' 69 | id: build 70 | args: ["./gradlew", "assembleDebug", "check", "detekt", "-PLAST_FM_APIKEY=\"${_LAST_FM_APIKEY}\""] 71 | <<: &env 72 | env: 73 | - 'TERM=dumb' 74 | - 'JAVA_TOOL_OPTIONS="-Xmx3g"' 75 | - 'GRADLE_USER_HOME=/build_cache/.gradle' 76 | - 'GRADLE_OPTS="-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=8 -Dkotlin.incremental=false"' 77 | - 'BRANCH_NAME=$BRANCH_NAME' 78 | waitFor: 79 | - extract_build_cache 80 | volumes: 81 | - name: 'build_cache' 82 | path: '/build_cache' 83 | 84 | # 85 | # Save the APKs 86 | # 87 | - name: 'gcr.io/cloud-builders/gsutil' 88 | args: ['-q', 'cp', '-r', 'app/build/outputs/apk', 'gs://${_ARTIFACT_BUCKET}/$BRANCH_NAME-$BUILD_ID/'] 89 | waitFor: ['build'] 90 | 91 | # 92 | # Cleanup 93 | # 94 | 95 | # Compress the gradle build cache 96 | - name: 'gcr.io/$PROJECT_ID/tar' 97 | id: compress_cache 98 | args: ['cpvzf', '/build_cache/cache.tgz', '/build_cache/.gradle/caches', '/build_cache/.gradle/wrapper'] 99 | waitFor: ['build'] 100 | volumes: 101 | - name: 'build_cache' 102 | path: '/build_cache' 103 | 104 | # Store the build cache 105 | - name: gcr.io/cloud-builders/gsutil 106 | args: ['cp', '/build_cache/cache.tgz', 'gs://${_CACHE_BUCKET}/cache.tgz'] 107 | waitFor: ['compress_cache'] 108 | volumes: 109 | - name: 'build_cache' 110 | path: '/build_cache' 111 | 112 | timeout: 1800s 113 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.gitlab.arturbosch.detekt.detekt 2 | 3 | plugins { 4 | id(BuildPlugins.androidLibrary) 5 | id(BuildPlugins.kotlinAndroid) 6 | id(BuildPlugins.kotlinAndroidExtensions) 7 | id(BuildPlugins.kotlinKapt) 8 | id(BuildPlugins.detekt) version (BuildPlugins.Versions.detekt) 9 | } 10 | 11 | android { 12 | compileSdkVersion(AndroidSdk.compile) 13 | 14 | defaultConfig { 15 | minSdkVersion(AndroidSdk.min) 16 | targetSdkVersion(AndroidSdk.target) 17 | 18 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | getByName("release") { 23 | isMinifyEnabled = false 24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 25 | } 26 | } 27 | compileOptions { 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation(Libraries.kotlinStdLib) 35 | api(Libraries.okHttp) 36 | api(Libraries.loggingInterceptor) 37 | implementation(Libraries.dagger) 38 | implementation(Libraries.daggerAndroid) 39 | kapt(Libraries.daggerCompiler) 40 | kapt(Libraries.daggerAndroidCompiler) 41 | api(Libraries.workManager) 42 | 43 | testImplementation(TestLibraries.junit4) 44 | } 45 | 46 | detekt { 47 | version = BuildPlugins.Versions.detekt 48 | input = files("src/main/java", "src/androidx/java", "src/support/java") 49 | filters = ".*test.*,.*/resources/.*,.*/tmp/.*" 50 | } 51 | -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/stylingandroid/muselee/core/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.core; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.stylingandroid.muselee.core.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/connectivity/ConnectivityLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.connectivity 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.MutableLiveData 5 | import javax.inject.Inject 6 | 7 | class ConnectivityLiveData @Inject constructor(context: Context) : 8 | MutableLiveData() { 9 | 10 | private val connectionMonitor = ConnectivityMonitor.getInstance(context.applicationContext) 11 | 12 | override fun onActive() { 13 | super.onActive() 14 | connectionMonitor.startListening(::setConnected) 15 | } 16 | 17 | override fun onInactive() { 18 | connectionMonitor.stopListening() 19 | super.onInactive() 20 | } 21 | 22 | private fun setConnected(isConnected: Boolean) = 23 | postValue(if (isConnected) ConnectivityState.Connected else ConnectivityState.Disconnected) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/connectivity/ConnectivityMonitor.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.connectivity 2 | 3 | import android.annotation.TargetApi 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.net.ConnectivityManager 9 | import android.net.Network 10 | import android.os.Build 11 | 12 | internal sealed class ConnectivityMonitor( 13 | protected val connectivityManager: ConnectivityManager 14 | ) { 15 | 16 | protected var callbackFunction: ((Boolean) -> Unit) = {} 17 | 18 | abstract fun startListening(callback: (Boolean) -> Unit) 19 | abstract fun stopListening() 20 | 21 | @TargetApi(Build.VERSION_CODES.N) 22 | private class NougatConnectivityMonitor(connectivityManager: ConnectivityManager) : 23 | ConnectivityMonitor(connectivityManager) { 24 | 25 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 26 | override fun onAvailable(network: Network) { 27 | super.onAvailable(network) 28 | callbackFunction(true) 29 | } 30 | 31 | override fun onLost(network: Network) { 32 | super.onLost(network) 33 | callbackFunction(false) 34 | } 35 | } 36 | 37 | override fun startListening(callback: (Boolean) -> Unit) { 38 | callbackFunction = callback 39 | callbackFunction(false) 40 | connectivityManager.registerDefaultNetworkCallback(networkCallback) 41 | } 42 | 43 | override fun stopListening() { 44 | connectivityManager.unregisterNetworkCallback(networkCallback) 45 | callbackFunction = {} 46 | } 47 | } 48 | 49 | @Suppress("Deprecation") 50 | private class LegacyConnectivityMonitor( 51 | private val context: Context, 52 | connectivityManager: ConnectivityManager 53 | ) : ConnectivityMonitor(connectivityManager) { 54 | 55 | private val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) 56 | 57 | private val isNetworkConnected: Boolean 58 | get() = connectivityManager.activeNetworkInfo?.isConnected == true 59 | 60 | override fun startListening(callback: (Boolean) -> Unit) { 61 | callbackFunction = callback 62 | callbackFunction(isNetworkConnected) 63 | context.registerReceiver(receiver, filter) 64 | } 65 | 66 | override fun stopListening() { 67 | context.unregisterReceiver(receiver) 68 | callbackFunction = {} 69 | } 70 | 71 | private val receiver = object : BroadcastReceiver() { 72 | override fun onReceive(context: Context, intent: Intent) { 73 | callbackFunction(isNetworkConnected) 74 | } 75 | } 76 | } 77 | 78 | companion object { 79 | fun getInstance(context: Context): ConnectivityMonitor { 80 | val connectivityManager = 81 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 82 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 83 | NougatConnectivityMonitor(connectivityManager) 84 | } else { 85 | LegacyConnectivityMonitor(context, connectivityManager) 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/connectivity/ConnectivityState.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.connectivity 2 | 3 | enum class ConnectivityState { 4 | Connected, 5 | Disconnected 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/di/BaseViewModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import androidx.lifecycle.ViewModelProvider 4 | import com.stylingandroid.muselee.view.ViewModelFactory 5 | import dagger.Binds 6 | import dagger.Module 7 | import javax.inject.Singleton 8 | 9 | @Module 10 | @Suppress("unused") 11 | abstract class BaseViewModule { 12 | 13 | @Singleton 14 | @Binds 15 | abstract fun bindViewModelFactory(factory: ViewModelFactory) : ViewModelProvider.Factory 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/di/CoreNetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import com.stylingandroid.muselee.core.BuildConfig 4 | import dagger.Module 5 | import dagger.Provides 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | 9 | @Module 10 | object CoreNetworkModule { 11 | 12 | @Provides 13 | @JvmStatic 14 | internal fun providesLoggingInterceptor(): HttpLoggingInterceptor? = 15 | if (BuildConfig.DEBUG) { 16 | HttpLoggingInterceptor().apply { 17 | level = HttpLoggingInterceptor.Level.BODY 18 | } 19 | } else null 20 | 21 | @Provides 22 | @JvmStatic 23 | internal fun providesOkHttpClientBuilder( 24 | loggingInterceptor: HttpLoggingInterceptor? 25 | ): OkHttpClient.Builder = 26 | OkHttpClient.Builder() 27 | .apply { 28 | loggingInterceptor?.also { 29 | addInterceptor(it) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/di/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @Target( 8 | AnnotationTarget.FUNCTION, 9 | AnnotationTarget.PROPERTY_GETTER, 10 | AnnotationTarget.PROPERTY_SETTER 11 | ) 12 | @Retention(AnnotationRetention.RUNTIME) 13 | @MapKey 14 | annotation class ViewModelKey(val value: KClass) 15 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/di/WorkerKey.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.di 2 | 3 | import androidx.work.ListenableWorker 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @MapKey 8 | @Target(AnnotationTarget.FUNCTION) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | annotation class WorkerKey(val value: KClass) 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/providers/DataMapper.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.providers 2 | 3 | interface DataMapper { 4 | fun encode(source: S): R 5 | fun decode(source: R): S = throw NotImplementedError() 6 | } 7 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/providers/DataPersister.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.providers 2 | 3 | interface DataPersister : DataProvider { 4 | fun persistData(data: T) 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/providers/DataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.providers 2 | 3 | interface DataProvider { 4 | 5 | fun requestData(callback: (item: T) -> Unit) 6 | 7 | fun requestData(): T = throw NotImplementedError() 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/providers/UpdateScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.providers 2 | 3 | interface UpdateScheduler { 4 | fun scheduleUpdate(items: List) 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/view/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.view 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class ViewModelFactory @Inject constructor( 11 | private val viewModelProviders: Map, @JvmSuppressWildcards Provider> 12 | ) : ViewModelProvider.Factory { 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(modelClass: Class): T { 16 | val provider = viewModelProviders[modelClass] 17 | ?: viewModelProviders.entries.first { modelClass.isAssignableFrom(it.key) }.value 18 | 19 | return provider.get() as T 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/stylingandroid/muselee/work/DaggerWorkerFactory.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.work 2 | 3 | import android.content.Context 4 | import androidx.work.ListenableWorker 5 | import androidx.work.WorkerFactory 6 | import androidx.work.WorkerParameters 7 | import javax.inject.Inject 8 | import javax.inject.Provider 9 | 10 | class DaggerWorkerFactory @Inject constructor( 11 | private val workerFactories: Map, @JvmSuppressWildcards Provider> 12 | ) : WorkerFactory() { 13 | 14 | override fun createWorker( 15 | appContext: Context, 16 | workerClassName: String, 17 | workerParameters: WorkerParameters 18 | ): ListenableWorker? { 19 | val foundEntry = 20 | workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) } 21 | return foundEntry?.value?.get()?.create(appContext, workerParameters) 22 | } 23 | 24 | interface ChildWorkerFactory { 25 | fun create(appContext: Context, params: WorkerParameters): ListenableWorker 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/res/font/muli_extrabold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/font/muli_semibold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/values/font_certs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @array/com_google_android_gms_fonts_certs_dev 5 | @array/com_google_android_gms_fonts_certs_prod 6 | 7 | 8 | 9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= 10 | 11 | 12 | 13 | 14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /core/src/main/res/values/preloaded_fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | @font/muli_extrabold 6 | @font/muli_semibold 7 | 8 | 9 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | core 3 | 4 | -------------------------------------------------------------------------------- /core/src/test/java/com/stylingandroid/muselee/core/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.core; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/7d7e4096e16057e8667446d8a6a4a87c5742ed5d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 27 15:19:41 GMT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-rc-2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':core', ':topartists' 2 | -------------------------------------------------------------------------------- /topartists/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /topartists/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import java.io.FileInputStream 3 | import java.util.Properties 4 | 5 | plugins { 6 | id(BuildPlugins.androidLibrary) 7 | id(BuildPlugins.kotlinAndroid) 8 | id(BuildPlugins.kotlinAndroidExtensions) 9 | id(BuildPlugins.kotlinKapt) 10 | id(BuildPlugins.detekt) version (BuildPlugins.Versions.detekt) 11 | } 12 | 13 | val props = Properties() 14 | val localProperties: File = project.rootProject.file("local.properties") 15 | if (localProperties.exists()) { 16 | props.load(FileInputStream(localProperties)) 17 | } 18 | if (!props.containsKey("last.fm.apikey")){ 19 | props["last.fm.apikey"] = rootProject.properties["LAST_FM_APIKEY"] 20 | } 21 | 22 | android { 23 | compileSdkVersion(AndroidSdk.compile) 24 | 25 | defaultConfig { 26 | minSdkVersion(AndroidSdk.min) 27 | targetSdkVersion(AndroidSdk.target) 28 | 29 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 30 | 31 | buildConfigField("String", "LAST_FM_APIKEY", props["last.fm.apikey"] as String) 32 | } 33 | 34 | buildTypes { 35 | getByName("release") { 36 | isMinifyEnabled = false 37 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 38 | } 39 | } 40 | compileOptions { 41 | targetCompatibility = JavaVersion.VERSION_1_8 42 | sourceCompatibility = JavaVersion.VERSION_1_8 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation(project(ProjectModules.core)) 48 | 49 | implementation(Libraries.kotlinStdLib) 50 | api(Libraries.kotlinCoroutines) 51 | api(Libraries.kotlinCoroutinesAndroid) 52 | implementation(Libraries.appCompat) 53 | implementation(Libraries.materialComponents) 54 | implementation(Libraries.ktxCore) 55 | implementation(Libraries.constraintLayout) 56 | implementation(Libraries.archLifecycle) 57 | api(Libraries.archRoomRuntime) 58 | kapt(Libraries.archRoomCompiler) 59 | implementation(Libraries.dagger) 60 | implementation(Libraries.daggerAndroid) 61 | implementation(Libraries.glide) 62 | api(Libraries.retrofit) 63 | implementation(Libraries.retrofitMoshi) 64 | kapt(Libraries.daggerCompiler) 65 | kapt(Libraries.daggerAndroidCompiler) 66 | kapt(Libraries.glideCompiler) 67 | 68 | testImplementation(TestLibraries.junit4) 69 | } 70 | 71 | detekt { 72 | version = BuildPlugins.Versions.detekt 73 | input = files("src/main/java", "src/androidx/java", "src/support/java") 74 | filters = ".*test.*,.*/resources/.*,.*/tmp/.*" 75 | } 76 | -------------------------------------------------------------------------------- /topartists/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /topartists/src/androidTest/java/com/stylingandroid/muselee/topartists/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.stylingandroid.muselee.topartists.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /topartists/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/database/DatabaseTopArtistsMapper.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.database 2 | 3 | import com.stylingandroid.muselee.providers.DataMapper 4 | import com.stylingandroid.muselee.topartists.entities.Artist 5 | 6 | class DatabaseTopArtistsMapper : DataMapper, Pair>> { 7 | 8 | override fun encode(source: Triple): Pair> { 9 | val (rank, artist, created) = source 10 | return DbArtist( 11 | rank, 12 | artist.name, 13 | created, 14 | artist.expiry 15 | ) to artist.images.map { DbImage(rank, it.key.ordinal, it.value) } 16 | } 17 | 18 | 19 | override fun decode(source: Pair>): Triple { 20 | val (artist, images) = source 21 | return Triple( 22 | artist.rank, 23 | Artist( 24 | artist.name, 25 | images.map { Artist.ImageSize.values()[it.typeIndex] to it.url }.toMap(), 26 | artist.expiry 27 | ), 28 | artist.created 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/database/DatabaseTopArtistsPersister.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.database 2 | 3 | import com.stylingandroid.muselee.providers.DataMapper 4 | import com.stylingandroid.muselee.providers.DataPersister 5 | import com.stylingandroid.muselee.topartists.entities.Artist 6 | 7 | 8 | class DatabaseTopArtistsPersister( 9 | private val dao: TopArtistsDao, 10 | private val mapper: DataMapper, Pair>> 11 | ) : DataPersister> { 12 | 13 | override fun persistData(data: List) { 14 | dao.deleteAll() 15 | val now = System.currentTimeMillis() 16 | val dbData = data.mapIndexed { index, artist -> 17 | mapper.encode(Triple(index, artist, now)) 18 | } 19 | dao.insertTopArtists(dbData.map { it.first }) 20 | dao.insertImages(dbData.flatMap { it.second }) 21 | } 22 | 23 | override fun requestData(callback: (item: List) -> Unit) { 24 | dao.deleteOutdated(System.currentTimeMillis()) 25 | val dbImages = dao.getAllImages() 26 | val artists = dao.getAllArtists().sortedBy { it.rank }.map { artist -> 27 | mapper.decode(artist to dbImages.filter { it.rank == artist.rank }).second 28 | } 29 | callback(artists) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/database/DbArtist.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.ForeignKey 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | 8 | @Entity(indices = [Index(value = ["name"])]) 9 | data class DbArtist( 10 | @PrimaryKey val rank: Int, 11 | val name: String, 12 | val created: Long, 13 | val expiry: Long 14 | ) 15 | 16 | @Entity( 17 | primaryKeys = ["rank", "typeIndex"], 18 | foreignKeys = [ 19 | ForeignKey( 20 | entity = DbArtist::class, 21 | parentColumns = ["rank"], 22 | childColumns = ["rank"], 23 | onDelete = ForeignKey.CASCADE 24 | ) 25 | ] 26 | ) 27 | data class DbImage( 28 | val rank: Int, 29 | val typeIndex: Int, 30 | val url: String 31 | ) 32 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/database/TopArtistsDao.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | 7 | @Dao 8 | interface TopArtistsDao { 9 | 10 | @Insert 11 | fun insertTopArtists(artists: List) 12 | 13 | @Insert 14 | fun insertImages(artists: List) 15 | 16 | @Query("SELECT * FROM DbArtist") 17 | fun getAllArtists(): List 18 | 19 | @Query("SELECT * FROM DbImage") 20 | fun getAllImages(): List 21 | 22 | @Query("DELETE FROM DbArtist WHERE expiry < :target") 23 | fun deleteOutdated(target: Long) 24 | 25 | @Query("DELETE From DbArtist") 26 | fun deleteAll() 27 | } 28 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/database/TopArtistsDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [DbArtist::class, DbImage::class], version = 1, exportSchema = false) 7 | abstract class TopArtistsDatabase : RoomDatabase() { 8 | 9 | abstract fun topArtistsDao(): TopArtistsDao 10 | } 11 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.stylingandroid.muselee.providers.DataMapper 6 | import com.stylingandroid.muselee.providers.DataPersister 7 | import com.stylingandroid.muselee.topartists.database.DatabaseTopArtistsMapper 8 | import com.stylingandroid.muselee.topartists.database.DatabaseTopArtistsPersister 9 | import com.stylingandroid.muselee.topartists.database.DbArtist 10 | import com.stylingandroid.muselee.topartists.database.DbImage 11 | import com.stylingandroid.muselee.topartists.database.TopArtistsDao 12 | import com.stylingandroid.muselee.topartists.database.TopArtistsDatabase 13 | import com.stylingandroid.muselee.topartists.entities.Artist 14 | import dagger.Module 15 | import dagger.Provides 16 | 17 | @Module 18 | object DatabaseModule { 19 | 20 | @Provides 21 | @JvmStatic 22 | internal fun providesDatabase(context: Application): TopArtistsDatabase = 23 | Room.databaseBuilder(context, TopArtistsDatabase::class.java, "top-artists").build() 24 | 25 | @Provides 26 | @JvmStatic 27 | internal fun providesTopArtistsDao(database: TopArtistsDatabase): TopArtistsDao = 28 | database.topArtistsDao() 29 | 30 | @Provides 31 | @JvmStatic 32 | internal fun providesTopArtistsMapper(): 33 | DataMapper, Pair>> = 34 | DatabaseTopArtistsMapper() 35 | 36 | @Provides 37 | @JvmStatic 38 | internal fun providesDatabasePersister( 39 | dao: TopArtistsDao, 40 | mapper: DataMapper, Pair>> 41 | ): DataPersister> = 42 | DatabaseTopArtistsPersister(dao, mapper) 43 | } 44 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/EntitiesModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import com.stylingandroid.muselee.providers.DataPersister 4 | import com.stylingandroid.muselee.providers.DataProvider 5 | import com.stylingandroid.muselee.providers.UpdateScheduler 6 | import com.stylingandroid.muselee.topartists.entities.Artist 7 | import com.stylingandroid.muselee.topartists.entities.TopArtistsRepository 8 | import com.stylingandroid.muselee.topartists.entities.TopArtistsState 9 | import dagger.Module 10 | import dagger.Provides 11 | import javax.inject.Named 12 | 13 | @Module 14 | object EntitiesModule { 15 | 16 | @Provides 17 | @Named(TopArtistsModule.ENTITIES) 18 | @JvmStatic 19 | internal fun providesTopArtistsRepository( 20 | persistence: DataPersister>, 21 | @Named(TopArtistsModule.NETWORK) provider: DataProvider, 22 | scheduler: UpdateScheduler 23 | ): DataProvider = TopArtistsRepository(persistence, provider, scheduler) 24 | } 25 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/LastFmTopArtistsModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import com.stylingandroid.muselee.providers.DataMapper 4 | import com.stylingandroid.muselee.providers.DataProvider 5 | import com.stylingandroid.muselee.topartists.entities.Artist 6 | import com.stylingandroid.muselee.topartists.entities.TopArtistsState 7 | import com.stylingandroid.muselee.topartists.net.LastFmArtists 8 | import com.stylingandroid.muselee.topartists.net.LastFmArtistsMapper 9 | import com.stylingandroid.muselee.topartists.net.LastFmTopArtistsApi 10 | import com.stylingandroid.muselee.topartists.net.LastFmTopArtistsProvider 11 | import dagger.Module 12 | import dagger.Provides 13 | import javax.inject.Named 14 | 15 | @Module 16 | object LastFmTopArtistsModule { 17 | 18 | @Provides 19 | @Named(TopArtistsModule.NETWORK) 20 | @JvmStatic 21 | fun providesTopArtistsDataProvider( 22 | lastFmTopArtistsApi: LastFmTopArtistsApi, 23 | mapper: DataMapper, List> 24 | ): DataProvider = 25 | LastFmTopArtistsProvider( 26 | lastFmTopArtistsApi, 27 | mapper 28 | ) 29 | 30 | @Provides 31 | @JvmStatic 32 | fun providesLastFmMapper(): DataMapper, List> = 33 | LastFmArtistsMapper() 34 | } 35 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import com.stylingandroid.muselee.di.CoreNetworkModule 4 | import com.stylingandroid.muselee.topartists.BuildConfig 5 | import com.stylingandroid.muselee.topartists.net.LastFmTopArtistsApi 6 | import dagger.Module 7 | import dagger.Provides 8 | import okhttp3.Interceptor 9 | import okhttp3.OkHttpClient 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.moshi.MoshiConverterFactory 12 | import javax.inject.Named 13 | import javax.inject.Singleton 14 | 15 | @Module(includes = [CoreNetworkModule::class]) 16 | object NetworkModule { 17 | 18 | @Provides 19 | @Named("API_KEY") 20 | @JvmStatic 21 | internal fun providesApiKey() = 22 | Interceptor { chain -> 23 | val newRequest = chain.request().let { request -> 24 | val newUrl = request.url().newBuilder() 25 | .addQueryParameter("api_key", BuildConfig.LAST_FM_APIKEY) 26 | .build() 27 | request.newBuilder() 28 | .url(newUrl) 29 | .build() 30 | } 31 | chain.proceed(newRequest) 32 | } 33 | 34 | @Provides 35 | @Named("JSON") 36 | @JvmStatic 37 | internal fun providesJson() = 38 | Interceptor { chain -> 39 | val newRequest = chain.request().let { request -> 40 | val newUrl = request.url().newBuilder() 41 | .addQueryParameter("format", "json") 42 | .build() 43 | request.newBuilder() 44 | .url(newUrl) 45 | .build() 46 | } 47 | chain.proceed(newRequest) 48 | } 49 | 50 | @Provides 51 | @JvmStatic 52 | internal fun providesOkHttpClient( 53 | builder: OkHttpClient.Builder, 54 | @Named("API_KEY") apiKeyInterceptor: Interceptor, 55 | @Named("JSON") jsonInterceptor: Interceptor 56 | ): OkHttpClient = 57 | builder.addInterceptor(apiKeyInterceptor) 58 | .addInterceptor(jsonInterceptor) 59 | .build() 60 | 61 | @Provides 62 | @Singleton 63 | @JvmStatic 64 | internal fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit = 65 | Retrofit.Builder() 66 | .baseUrl("https://ws.audioscrobbler.com/2.0/") 67 | .client(okHttpClient) 68 | .addConverterFactory(MoshiConverterFactory.create()) 69 | .build() 70 | 71 | @Provides 72 | @JvmStatic 73 | internal fun providesLastFmTopArtistsApi(retrofit: Retrofit): LastFmTopArtistsApi = 74 | retrofit.create(LastFmTopArtistsApi::class.java) 75 | } 76 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/SchedulerModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import com.stylingandroid.muselee.providers.UpdateScheduler 4 | import com.stylingandroid.muselee.topartists.entities.Artist 5 | import com.stylingandroid.muselee.topartists.scheduler.TopArtistsScheduler 6 | import dagger.Module 7 | import dagger.Provides 8 | 9 | @Module 10 | object SchedulerModule { 11 | 12 | @Provides 13 | @JvmStatic 14 | fun providesScheduler(): UpdateScheduler = 15 | TopArtistsScheduler() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/di/TopArtistsModule.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.stylingandroid.muselee.di.BaseViewModule 5 | import com.stylingandroid.muselee.di.ViewModelKey 6 | import com.stylingandroid.muselee.di.WorkerKey 7 | import com.stylingandroid.muselee.topartists.scheduler.TopArtistsUpdateWorker 8 | import com.stylingandroid.muselee.topartists.view.TopArtistsFragment 9 | import com.stylingandroid.muselee.topartists.view.TopArtistsViewModel 10 | import com.stylingandroid.muselee.work.DaggerWorkerFactory 11 | import dagger.Binds 12 | import dagger.Module 13 | import dagger.android.ContributesAndroidInjector 14 | import dagger.multibindings.IntoMap 15 | 16 | @Module( 17 | includes = [ 18 | EntitiesModule::class, 19 | DatabaseModule::class, 20 | NetworkModule::class, 21 | BaseViewModule::class, 22 | SchedulerModule::class, 23 | LastFmTopArtistsModule::class 24 | ] 25 | ) 26 | @Suppress("unused") 27 | abstract class TopArtistsModule { 28 | 29 | companion object { 30 | const val ENTITIES = "ENTITIES" 31 | const val NETWORK = "NETWORK" 32 | } 33 | 34 | @ContributesAndroidInjector 35 | abstract fun bindTopArtistsFragment(): TopArtistsFragment 36 | 37 | @Binds 38 | @IntoMap 39 | @ViewModelKey(TopArtistsViewModel::class) 40 | abstract fun bindChartsViewModel(viewModel: TopArtistsViewModel): ViewModel 41 | 42 | @Binds 43 | @IntoMap 44 | @WorkerKey(TopArtistsUpdateWorker::class) 45 | abstract fun bindTopArtistsUpdateWorker(factory: TopArtistsUpdateWorker.Factory): 46 | DaggerWorkerFactory.ChildWorkerFactory 47 | } 48 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/entities/Artist.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.entities 2 | 3 | data class Artist(val name: String, val images: Map, val expiry: Long) { 4 | 5 | enum class ImageSize { 6 | SMALL, 7 | MEDIUM, 8 | LARGE, 9 | EXTRA_LARGE, 10 | UNKNOWN 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/entities/TopArtistsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.entities 2 | 3 | import com.stylingandroid.muselee.providers.DataPersister 4 | import com.stylingandroid.muselee.providers.DataProvider 5 | import com.stylingandroid.muselee.providers.UpdateScheduler 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.launch 9 | 10 | class TopArtistsRepository( 11 | private val persister: DataPersister>, 12 | private val provider: DataProvider, 13 | private val scheduler: UpdateScheduler 14 | ) : DataProvider { 15 | 16 | override fun requestData(callback: (item: TopArtistsState) -> Unit) = 17 | persister.requestData { artists -> 18 | if (artists.isEmpty()) { 19 | provider.requestData { state -> 20 | if (state is TopArtistsState.Success) { 21 | GlobalScope.launch(Dispatchers.IO) { 22 | persister.persistData(state.artists) 23 | } 24 | scheduler.scheduleUpdate(state.artists) 25 | } 26 | callback(state) 27 | } 28 | } else { 29 | callback(TopArtistsState.Success(artists)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/entities/TopArtistsState.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.entities 2 | 3 | sealed class TopArtistsState { 4 | 5 | object Loading : TopArtistsState() 6 | 7 | class Success(val artists: List) : TopArtistsState() 8 | 9 | class Error(val message: String) : TopArtistsState() 10 | } 11 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/net/LastFm.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.net 2 | 3 | import com.squareup.moshi.Json 4 | 5 | data class LastFmArtists(val artists: ArtistsList) 6 | 7 | data class ArtistsList(val artist: List) 8 | 9 | data class LastFmArtist( 10 | val name: String, 11 | @field:Json(name = "playcount") val playCount: Long, 12 | val listeners: Long, 13 | val mbid: String, 14 | val url: String, 15 | val streamable: Int, 16 | @field:Json(name = "image") val images: List 17 | ) 18 | 19 | data class LastFmImage( 20 | @field:Json(name = "#text") val url: String, 21 | val size: String 22 | ) 23 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/net/LastFmArtistsMapper.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.net 2 | 3 | import com.stylingandroid.muselee.providers.DataMapper 4 | import com.stylingandroid.muselee.topartists.entities.Artist 5 | import com.stylingandroid.muselee.topartists.entities.Artist.ImageSize 6 | 7 | class LastFmArtistsMapper : DataMapper, List> { 8 | 9 | override fun encode(source: Pair): List { 10 | val (lastFmArtists, expiry) = source 11 | return lastFmArtists.artists.artist.map { artist -> 12 | Artist(artist.name, artist.normalisedImages(), expiry) 13 | } 14 | } 15 | 16 | private fun LastFmArtist.normalisedImages() = 17 | images.map { it.size.toImageSize() to it.url }.toMap() 18 | 19 | private fun String.toImageSize(): ImageSize = 20 | when (this) { 21 | "small" -> ImageSize.SMALL 22 | "medium" -> ImageSize.MEDIUM 23 | "large" -> ImageSize.LARGE 24 | "extralarge" -> ImageSize.EXTRA_LARGE 25 | else -> ImageSize.UNKNOWN 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/net/LastFmTopArtistsApi.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.net 2 | 3 | import retrofit2.Call 4 | import retrofit2.http.GET 5 | 6 | interface LastFmTopArtistsApi { 7 | 8 | @GET("?method=chart.gettopartists") 9 | fun getTopArtists(): Call 10 | } 11 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/net/LastFmTopArtistsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.net 2 | 3 | import com.stylingandroid.muselee.providers.DataMapper 4 | import com.stylingandroid.muselee.providers.DataProvider 5 | import com.stylingandroid.muselee.topartists.entities.Artist 6 | import com.stylingandroid.muselee.topartists.entities.TopArtistsState 7 | import okhttp3.internal.http.HttpDate 8 | import retrofit2.Call 9 | import retrofit2.Callback 10 | import retrofit2.Response 11 | import java.util.concurrent.TimeUnit 12 | 13 | class LastFmTopArtistsProvider( 14 | private val topArtistsApi: LastFmTopArtistsApi, 15 | private val mapper: DataMapper, List> 16 | ) : DataProvider { 17 | 18 | override fun requestData(): TopArtistsState { 19 | val response = topArtistsApi.getTopArtists().execute() 20 | return response.takeIf { it.isSuccessful }?.body()?.let { artists -> 21 | TopArtistsState.Success(mapper.encode(artists to response.expiry)) 22 | } ?: TopArtistsState.Error(response.errorBody()?.string() ?: "Network Error") 23 | } 24 | 25 | override fun requestData(callback: (topArtists: TopArtistsState) -> Unit) { 26 | callback(TopArtistsState.Loading) 27 | topArtistsApi.getTopArtists().enqueue(object : Callback { 28 | 29 | override fun onFailure(call: Call, t: Throwable) { 30 | callback(TopArtistsState.Error(t.localizedMessage ?: t.toString())) 31 | } 32 | 33 | override fun onResponse(call: Call, response: Response) { 34 | response.body()?.also { topArtists -> 35 | callback(TopArtistsState.Success(mapper.encode(topArtists to response.expiry))) 36 | } 37 | } 38 | }) 39 | } 40 | 41 | private val Response.expiry: Long 42 | get() { 43 | val expires: Long? = if (headers().names().contains(HEADER_EXPIRES)) { 44 | HttpDate.parse(headers().get(HEADER_EXPIRES)).time 45 | } else null 46 | val cacheControlMaxAge = raw().cacheControl().maxAgeSeconds().toLong() 47 | val maxAge: Long? = 48 | cacheControlMaxAge.takeIf { it >= 0 } ?: headers().get(HEADER_AC_MAX_AGE)?.toLong() 49 | val date = if (headers().names().contains(HEADER_DATE)) { 50 | HttpDate.parse(headers().get(HEADER_DATE)).time 51 | } else { 52 | System.currentTimeMillis() 53 | } 54 | return expires 55 | ?: maxAge?.let { date + TimeUnit.SECONDS.toMillis(it) } 56 | ?: date + TimeUnit.DAYS.toMillis(1) 57 | } 58 | 59 | companion object { 60 | private const val HEADER_DATE = "Date" 61 | private const val HEADER_EXPIRES = "Expires" 62 | private const val HEADER_AC_MAX_AGE = "Access-Control-Max-Age" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/scheduler/TopArtistsScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.scheduler 2 | 3 | import androidx.work.Constraints 4 | import androidx.work.ExistingWorkPolicy 5 | import androidx.work.NetworkType 6 | import androidx.work.OneTimeWorkRequestBuilder 7 | import androidx.work.WorkManager 8 | import com.stylingandroid.muselee.providers.UpdateScheduler 9 | import com.stylingandroid.muselee.topartists.entities.Artist 10 | import java.util.concurrent.TimeUnit 11 | 12 | class TopArtistsScheduler : UpdateScheduler { 13 | 14 | override fun scheduleUpdate(items: List) { 15 | WorkManager.getInstance() 16 | .enqueueUniqueWork( 17 | UNIQUE_WORK_ID, 18 | ExistingWorkPolicy.REPLACE, 19 | OneTimeWorkRequestBuilder() 20 | .setInitialDelay(items.earliestUpdate(), TimeUnit.MILLISECONDS) 21 | .setConstraints( 22 | Constraints.Builder() 23 | .setRequiredNetworkType(NetworkType.UNMETERED) 24 | .setRequiresBatteryNotLow(true) 25 | .build() 26 | ) 27 | .build() 28 | ) 29 | } 30 | 31 | private fun List.earliestUpdate() = 32 | (minBy { it.expiry }?.expiry?.let { it - System.currentTimeMillis() } 33 | ?: TimeUnit.DAYS.toMillis(1)) / 2 34 | 35 | companion object { 36 | private const val UNIQUE_WORK_ID: String = "TopArtistsScheduler" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/scheduler/TopArtistsUpdateWorker.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.scheduler 2 | 3 | import android.content.Context 4 | import androidx.work.ListenableWorker 5 | import androidx.work.Worker 6 | import androidx.work.WorkerParameters 7 | import com.stylingandroid.muselee.providers.DataPersister 8 | import com.stylingandroid.muselee.providers.DataProvider 9 | import com.stylingandroid.muselee.providers.UpdateScheduler 10 | import com.stylingandroid.muselee.topartists.di.TopArtistsModule 11 | import com.stylingandroid.muselee.topartists.entities.Artist 12 | import com.stylingandroid.muselee.topartists.entities.TopArtistsState 13 | import com.stylingandroid.muselee.work.DaggerWorkerFactory 14 | import javax.inject.Inject 15 | import javax.inject.Named 16 | 17 | class TopArtistsUpdateWorker( 18 | private val provider: DataProvider, 19 | private val persister: DataPersister>, 20 | private val scheduler: UpdateScheduler, 21 | context: Context, 22 | workerParams: WorkerParameters 23 | ) : Worker(context, workerParams) { 24 | 25 | override fun doWork(): Result = 26 | when(val state = provider.requestData()) { 27 | is TopArtistsState.Success -> { 28 | persister.persistData(state.artists) 29 | scheduler.scheduleUpdate(state.artists) 30 | Result.success() 31 | } 32 | is TopArtistsState.Error -> Result.retry() 33 | is TopArtistsState.Loading -> throw IllegalStateException("Unexpected Loading State") 34 | } 35 | 36 | class Factory @Inject constructor( 37 | @Named(TopArtistsModule.NETWORK) private val provider: DataProvider, 38 | private val persister: DataPersister>, 39 | private val scheduler: UpdateScheduler 40 | ) : DaggerWorkerFactory.ChildWorkerFactory { 41 | 42 | override fun create(appContext: Context, params: WorkerParameters): ListenableWorker = 43 | TopArtistsUpdateWorker(provider, persister, scheduler, appContext, params) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/GridPositionCalculator.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | 5 | internal class GridPositionCalculator(var itemCount: Int) : GridLayoutManager.SpanSizeLookup() { 6 | 7 | companion object { 8 | 9 | private val doubleItems: IntRange = (1..10) 10 | const val fullSpanSize = 6 11 | private const val doubleSpanCount = 2 12 | private const val tripleSpanCount = 3 13 | private const val doubleSpanSize: Int = fullSpanSize / doubleSpanCount 14 | private const val tripleSpanSize: Int = fullSpanSize / tripleSpanCount 15 | } 16 | 17 | override fun getSpanSize(position: Int): Int = 18 | when (position) { 19 | 0 -> fullSpanSize 20 | in doubleItems -> doubleSpanSize 21 | else -> tripleSpanSize 22 | } 23 | 24 | fun getViewSize(position: Int): ViewSize = 25 | when (position) { 26 | 0 -> ViewSize.FULL 27 | in doubleItems -> ViewSize.DOUBLE 28 | else -> ViewSize.TRIPLE 29 | } 30 | 31 | fun isEndItem(position: Int): Boolean = 32 | when (position) { 33 | 0 -> true 34 | in doubleItems -> (position - doubleItems.start).rem(doubleSpanCount) != 0 35 | else -> (position - doubleItems.last).rem(tripleSpanCount) == 0 36 | } 37 | 38 | fun isInFinalBank(position: Int): Boolean = 39 | position >= itemCount - tripleSpanCount 40 | } 41 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.stylingandroid.muselee.topartists.R 8 | import com.stylingandroid.muselee.topartists.entities.Artist 9 | import com.stylingandroid.muselee.topartists.entities.Artist.ImageSize 10 | 11 | internal class TopArtistsAdapter( 12 | private val calculator: GridPositionCalculator, 13 | private val items: MutableList = mutableListOf() 14 | ) : RecyclerView.Adapter() { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopArtistsViewHolder = 17 | TopArtistsViewHolder( 18 | LayoutInflater.from(parent.context) 19 | .inflate(getLayoutId(viewType), parent, false) 20 | ) 21 | 22 | override fun getItemViewType(position: Int): Int = 23 | calculator.getViewSize(position).ordinal 24 | 25 | private fun getLayoutId(viewType: Int) = 26 | when(viewType) { 27 | ViewSize.FULL.ordinal -> R.layout.item_chart_artist_full 28 | ViewSize.DOUBLE.ordinal -> R.layout.item_chart_artist_medium 29 | else -> R.layout.item_chart_artist_small 30 | } 31 | 32 | override fun getItemCount(): Int = items.size 33 | 34 | override fun onBindViewHolder(holder: TopArtistsViewHolder, position: Int) { 35 | items[position].also { artist -> 36 | val imageSize: ImageSize = when (calculator.getViewSize(position)) { 37 | ViewSize.FULL, ViewSize.DOUBLE -> ImageSize.EXTRA_LARGE 38 | ViewSize.TRIPLE -> ImageSize.LARGE 39 | } 40 | holder.bind( 41 | rank = (position + 1).toString(), 42 | artistName = artist.name, 43 | artistImageUrl = artist.images[imageSize] ?: artist.images.values.first() 44 | ) 45 | } 46 | } 47 | 48 | fun replace(artists: List) { 49 | val difference = DiffUtil.calculateDiff(TopArtistsDiffUtil(items, artists)) 50 | items.clear() 51 | items += artists 52 | calculator.itemCount = items.size 53 | difference.dispatchUpdatesTo(this) 54 | } 55 | 56 | private class TopArtistsDiffUtil( 57 | private val oldList: List, 58 | private val newList: List 59 | ) : DiffUtil.Callback() { 60 | 61 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = 62 | oldList[oldItemPosition] === newList[newItemPosition] 63 | 64 | override fun getOldListSize(): Int = oldList.size 65 | 66 | override fun getNewListSize(): Int = newList.size 67 | 68 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = 69 | oldList[oldItemPosition].name == newList[newItemPosition].name 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsDiffUtil.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.stylingandroid.muselee.topartists.entities.Artist 5 | 6 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import android.content.Intent 4 | import android.content.res.Configuration 5 | import android.os.Bundle 6 | import android.provider.Settings 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.ProgressBar 12 | import android.widget.TextView 13 | import androidx.core.os.BuildCompat 14 | import androidx.lifecycle.Observer 15 | import androidx.lifecycle.ViewModelProvider 16 | import androidx.lifecycle.ViewModelProviders 17 | import androidx.recyclerview.widget.GridLayoutManager 18 | import androidx.recyclerview.widget.RecyclerView 19 | import com.google.android.material.snackbar.Snackbar 20 | import com.stylingandroid.muselee.connectivity.ConnectivityState 21 | import com.stylingandroid.muselee.topartists.R 22 | import com.stylingandroid.muselee.topartists.entities.Artist 23 | import dagger.android.support.DaggerFragment 24 | import javax.inject.Inject 25 | 26 | class TopArtistsFragment : DaggerFragment() { 27 | 28 | @Inject 29 | lateinit var viewModelFactory: ViewModelProvider.Factory 30 | 31 | private lateinit var topArtistsViewModel: TopArtistsViewModel 32 | private lateinit var topArtistsAdapter: TopArtistsAdapter 33 | private val calculator = GridPositionCalculator(0) 34 | 35 | private lateinit var topArtistsRecyclerView: RecyclerView 36 | private lateinit var retryButton: Button 37 | private lateinit var progress: ProgressBar 38 | private lateinit var errorMessage: TextView 39 | 40 | private val connectivitySnackbar: Snackbar by lazy { 41 | Snackbar.make(topArtistsRecyclerView, R.string.no_connectivity, Snackbar.LENGTH_INDEFINITE) 42 | .setAction(R.string.network_settings) { 43 | val intent = if (BuildCompat.isAtLeastQ()) { 44 | Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY) 45 | } else { 46 | Intent(Settings.ACTION_WIRELESS_SETTINGS) 47 | } 48 | startActivity(intent) 49 | } 50 | } 51 | 52 | private var itemSpacing: Int = 0 53 | private val spanCount: Int = GridPositionCalculator.fullSpanSize 54 | 55 | @RecyclerView.Orientation 56 | private var orientation = RecyclerView.VERTICAL 57 | 58 | override fun onCreate(savedInstanceState: Bundle?) { 59 | super.onCreate(savedInstanceState) 60 | topArtistsViewModel = ViewModelProviders.of(this, viewModelFactory) 61 | .get(TopArtistsViewModel::class.java) 62 | topArtistsAdapter = TopArtistsAdapter(calculator) 63 | itemSpacing = resources.getDimension(R.dimen.item_spacing).toInt() 64 | } 65 | 66 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = 67 | inflater.inflate(R.layout.fragment_top_artists, container, false).also { view -> 68 | topArtistsRecyclerView = view.findViewById(R.id.top_artists) 69 | retryButton = view.findViewById(R.id.retry) 70 | progress = view.findViewById(R.id.progress) 71 | errorMessage = view.findViewById(R.id.error_message) 72 | orientation = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 73 | RecyclerView.HORIZONTAL 74 | else 75 | RecyclerView.VERTICAL 76 | topArtistsRecyclerView.apply { 77 | adapter = topArtistsAdapter 78 | layoutManager = GridLayoutManager(context, spanCount, orientation, false).apply { 79 | spanSizeLookup = calculator 80 | } 81 | addItemDecoration(TopArtistsItemDecoraton(orientation, itemSpacing, calculator)) 82 | } 83 | retryButton.setOnClickListener { 84 | topArtistsViewModel.load() 85 | } 86 | } 87 | 88 | override fun onResume() { 89 | super.onResume() 90 | topArtistsViewModel.connectivityLiveData.observe(this, Observer { newState -> connectivityChange(newState) }) 91 | topArtistsViewModel.topArtistsViewState.observe(this, Observer { newState -> viewStateChanged(newState) }) 92 | } 93 | 94 | private fun connectivityChange(connectivityState: ConnectivityState) { 95 | if (connectivityState == ConnectivityState.Connected) { 96 | connectivitySnackbar.dismiss() 97 | } else { 98 | connectivitySnackbar.show() 99 | } 100 | } 101 | 102 | private fun viewStateChanged(topArtistsViewState: TopArtistsViewState) { 103 | when (topArtistsViewState) { 104 | is TopArtistsViewState.InProgress -> setLoading() 105 | is TopArtistsViewState.ShowError -> setError(topArtistsViewState.message) 106 | is TopArtistsViewState.ShowTopArtists -> updateTopArtists(topArtistsViewState.topArtists) 107 | } 108 | } 109 | 110 | private fun setLoading() { 111 | progress.visibility = View.VISIBLE 112 | errorMessage.visibility = View.GONE 113 | retryButton.visibility = View.GONE 114 | topArtistsRecyclerView.visibility = View.GONE 115 | } 116 | 117 | private fun setError(message: String) { 118 | errorMessage.text = message 119 | retryButton.visibility = View.VISIBLE 120 | topArtistsRecyclerView.visibility = View.GONE 121 | progress.visibility = View.GONE 122 | errorMessage.visibility = View.VISIBLE 123 | } 124 | 125 | private fun updateTopArtists(topArtists: List) { 126 | progress.visibility = View.GONE 127 | errorMessage.visibility = View.GONE 128 | retryButton.visibility = View.GONE 129 | topArtistsRecyclerView.visibility = View.VISIBLE 130 | topArtistsAdapter.replace(topArtists) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsItemDecoraton.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | internal class TopArtistsItemDecoraton( 8 | @RecyclerView.Orientation val orientation: Int, 9 | private val itemSpacing: Int, 10 | private val calculator: GridPositionCalculator 11 | ) : RecyclerView.ItemDecoration() { 12 | 13 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 14 | val position = parent.getChildAdapterPosition(view) 15 | if (orientation == RecyclerView.HORIZONTAL) { 16 | getHorizontalOffsets(outRect, position) 17 | } else { 18 | getVerticalOffsets(outRect, position) 19 | } 20 | } 21 | 22 | private fun getHorizontalOffsets(outRect: Rect, position: Int) { 23 | outRect.bottom = if (calculator.isEndItem(position)) 0 else itemSpacing 24 | outRect.right = if (calculator.isInFinalBank(position)) 0 else itemSpacing 25 | } 26 | 27 | private fun getVerticalOffsets(outRect: Rect, position: Int) { 28 | outRect.right = if (calculator.isEndItem(position)) 0 else itemSpacing 29 | outRect.bottom = if (calculator.isInFinalBank(position)) 0 else itemSpacing 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.bumptech.glide.Glide 8 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade 9 | import com.stylingandroid.muselee.topartists.R 10 | 11 | class TopArtistsViewHolder( 12 | item: View, 13 | private val rankView: TextView = item.findViewById(R.id.rank), 14 | private val imageView: ImageView = item.findViewById(R.id.image), 15 | private val nameView: TextView = item.findViewById(R.id.name) 16 | ) : RecyclerView.ViewHolder(item) { 17 | 18 | fun bind(rank: String, artistName: String, artistImageUrl: String) { 19 | rankView.text = rank 20 | nameView.text = artistName 21 | Glide.with(imageView) 22 | .load(artistImageUrl) 23 | .transition(withCrossFade()) 24 | .into(imageView) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.stylingandroid.muselee.connectivity.ConnectivityLiveData 7 | import com.stylingandroid.muselee.providers.DataProvider 8 | import com.stylingandroid.muselee.topartists.di.TopArtistsModule 9 | import com.stylingandroid.muselee.topartists.entities.TopArtistsState 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.Job 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | import javax.inject.Inject 16 | import javax.inject.Named 17 | import kotlin.coroutines.CoroutineContext 18 | 19 | class TopArtistsViewModel @Inject constructor( 20 | @Named(TopArtistsModule.ENTITIES) private val topArtistsProvider: DataProvider, 21 | val connectivityLiveData: ConnectivityLiveData 22 | ) : ViewModel(), CoroutineScope { 23 | 24 | private val job = Job() 25 | override val coroutineContext: CoroutineContext 26 | get() = Dispatchers.Main + job 27 | 28 | private val mutableLiveData: MutableLiveData = MutableLiveData() 29 | 30 | val topArtistsViewState: LiveData 31 | get() = mutableLiveData 32 | 33 | init { 34 | load() 35 | } 36 | 37 | override fun onCleared() { 38 | super.onCleared() 39 | job.cancel() 40 | } 41 | 42 | fun load() = launch { 43 | withContext(Dispatchers.IO) { 44 | topArtistsProvider.requestData { artistsState -> 45 | update(artistsState) 46 | } 47 | } 48 | } 49 | 50 | private fun update(artistsState: TopArtistsState) = launch { 51 | withContext(Dispatchers.Main) { 52 | mutableLiveData.value = when (artistsState) { 53 | TopArtistsState.Loading -> TopArtistsViewState.InProgress 54 | is TopArtistsState.Error -> TopArtistsViewState.ShowError(artistsState.message) 55 | is TopArtistsState.Success -> TopArtistsViewState.ShowTopArtists(artistsState.artists) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/TopArtistsViewState.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | import com.stylingandroid.muselee.topartists.entities.Artist 4 | 5 | sealed class TopArtistsViewState { 6 | 7 | object InProgress : TopArtistsViewState() 8 | 9 | class ShowTopArtists(val topArtists: List) : TopArtistsViewState() 10 | 11 | class ShowError(val message: String) : TopArtistsViewState() 12 | } 13 | -------------------------------------------------------------------------------- /topartists/src/main/java/com/stylingandroid/muselee/topartists/view/ViewSize.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists.view 2 | 3 | internal enum class ViewSize { 4 | FULL, 5 | DOUBLE, 6 | TRIPLE 7 | } 8 | -------------------------------------------------------------------------------- /topartists/src/main/res/layout-land/item_chart_artist_full.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 35 | 36 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /topartists/src/main/res/layout-land/item_chart_artist_medium.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 35 | 36 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /topartists/src/main/res/layout-land/item_chart_artist_small.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 35 | 36 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /topartists/src/main/res/layout/fragment_top_artists.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |