├── README.md ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── stylingandroid │ │ │ │ └── muselee │ │ │ │ ├── di │ │ │ │ ├── ActivityModule.kt │ │ │ │ ├── ApplicationComponent.kt │ │ │ │ └── ApplicationModule.kt │ │ │ │ ├── MuseleeActivity.kt │ │ │ │ ├── MuseleeAppGlideModule.kt │ │ │ │ └── MuseleeApplication.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── stylingandroid │ │ │ └── muselee │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── core ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── preloaded_fonts.xml │ │ │ │ └── font_certs.xml │ │ │ └── font │ │ │ │ ├── muli_extrabold.xml │ │ │ │ └── muli_semibold.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── stylingandroid │ │ │ │ └── muselee │ │ │ │ ├── providers │ │ │ │ ├── UpdateScheduler.kt │ │ │ │ ├── DataPersister.kt │ │ │ │ ├── DataMapper.kt │ │ │ │ └── DataProvider.kt │ │ │ │ ├── connectivity │ │ │ │ ├── ConnectivityState.kt │ │ │ │ ├── ConnectivityLiveData.kt │ │ │ │ └── ConnectivityMonitor.kt │ │ │ │ ├── di │ │ │ │ ├── WorkerKey.kt │ │ │ │ ├── ViewModelKey.kt │ │ │ │ ├── BaseViewModule.kt │ │ │ │ └── CoreNetworkModule.kt │ │ │ │ ├── view │ │ │ │ └── ViewModelFactory.kt │ │ │ │ └── work │ │ │ │ └── DaggerWorkerFactory.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── stylingandroid │ │ │ └── muselee │ │ │ └── core │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── core │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle.kts ├── topartists ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ └── strings.xml │ │ │ ├── layout │ │ │ │ ├── fragment_top_artists.xml │ │ │ │ ├── item_chart_artist_full.xml │ │ │ │ ├── item_chart_artist_medium.xml │ │ │ │ └── item_chart_artist_small.xml │ │ │ └── layout-land │ │ │ │ ├── item_chart_artist_full.xml │ │ │ │ ├── item_chart_artist_medium.xml │ │ │ │ └── item_chart_artist_small.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── stylingandroid │ │ │ │ └── muselee │ │ │ │ └── topartists │ │ │ │ ├── view │ │ │ │ ├── ViewSize.kt │ │ │ │ ├── TopArtistsDiffUtil.kt │ │ │ │ ├── TopArtistsViewState.kt │ │ │ │ ├── TopArtistsViewHolder.kt │ │ │ │ ├── TopArtistsItemDecoraton.kt │ │ │ │ ├── GridPositionCalculator.kt │ │ │ │ ├── TopArtistsViewModel.kt │ │ │ │ ├── TopArtistsAdapter.kt │ │ │ │ └── TopArtistsFragment.kt │ │ │ │ ├── net │ │ │ │ ├── LastFmTopArtistsApi.kt │ │ │ │ ├── LastFm.kt │ │ │ │ ├── LastFmArtistsMapper.kt │ │ │ │ └── LastFmTopArtistsProvider.kt │ │ │ │ ├── entities │ │ │ │ ├── TopArtistsState.kt │ │ │ │ ├── Artist.kt │ │ │ │ └── TopArtistsRepository.kt │ │ │ │ ├── database │ │ │ │ ├── TopArtistsDatabase.kt │ │ │ │ ├── TopArtistsDao.kt │ │ │ │ ├── DbArtist.kt │ │ │ │ ├── DatabaseTopArtistsMapper.kt │ │ │ │ └── DatabaseTopArtistsPersister.kt │ │ │ │ ├── di │ │ │ │ ├── SchedulerModule.kt │ │ │ │ ├── EntitiesModule.kt │ │ │ │ ├── LastFmTopArtistsModule.kt │ │ │ │ ├── TopArtistsModule.kt │ │ │ │ ├── DatabaseModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ │ └── scheduler │ │ │ │ ├── TopArtistsScheduler.kt │ │ │ │ └── TopArtistsUpdateWorker.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── stylingandroid │ │ │ └── muselee │ │ │ └── topartists │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── stylingandroid │ │ └── muselee │ │ └── topartists │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle.kts ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── encodings.xml ├── compiler.xml ├── runConfigurations.xml ├── gradle.xml └── misc.xml ├── .gitignore ├── Jenkinsfile ├── gradle.properties ├── gradlew.bat ├── cloudbuild.yaml ├── gradlew └── LICENSE.md /README.md: -------------------------------------------------------------------------------- 1 | Muselee 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /topartists/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':core', ':topartists' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Muselee 3 | 4 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | core 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StylingAndroid/Muselee/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /topartists/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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/providers/DataPersister.kt: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.providers 2 | 3 | interface DataPersister : DataProvider { 4 | fun persistData(data: T) 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /topartists/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1E9618 4 | #4EE147 5 | #156912 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/res/values/preloaded_fonts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | @font/muli_extrabold 6 | @font/muli_semibold 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /topartists/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Retry 3 | Artist Image 4 | Settings 5 | You are currently offline\nPlease check your network settings. 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /core/src/main/res/font/muli_extrabold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/font/muli_semibold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /topartists/src/test/java/com/stylingandroid/muselee/topartists/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.stylingandroid.muselee.topartists; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /topartists/src/main/res/layout/fragment_top_artists.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |