diff --git a/.gitignore b/.gitignore
index 56cc642..9cbbd0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,8 @@ out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
+.DS_Store
+
# Gradle files
.gradle/
build/
@@ -30,6 +32,10 @@ proguard/
# Log Files
*.log
+# Android Studio
+.idea
+/.idea/
+
# Android Studio Navigation editor temp files
.navigation/
@@ -52,15 +58,15 @@ captures/
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
-#*.jks
-#*.keystore
+*.jks
+*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
-# google-services.json
+google-services.json
# Freeline
freeline.py
@@ -82,4 +88,4 @@ lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
-# lint/reports/
+lint/reports/
diff --git a/README.md b/README.md
index 24cffaa..fa3e2d9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,24 @@
-# Podcast-Player
\ No newline at end of file
+#### Podcast Player App
+
+Esse App foi desenvolvido a API [PodcastIndex.org API](https://podcastindex-org.github.io/docs-api/#overview), que é uma API gratuita que provem dados de Podcasts.
+
+Para poder rodar o APP, abra o seguinte arquivo: app/src/main/res/values/podcast_index_api_keys.xml. Nele você deve colocar o seu API secret e sua API key que dá para se obter gratuitamente no site acima.
+
+Esse APP foi desenvolvido seguindo a arquitetura MVC. Segue um diagrama demonstrando como está o app:
+
+
+
+
+Seguem prints do app rodando:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/X# b/X#
new file mode 100644
index 0000000..e69de29
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..b37c23e
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,68 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs.kotlin'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
+
+ defaultConfig {
+ applicationId "br.ufpe.cin.vrvs.podcastplayer"
+ minSdkVersion 22
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ dataBinding true
+ }
+}
+
+dependencies {
+ implementation "androidx.appcompat:appcompat:$versions.appcompat"
+ implementation "androidx.constraintlayout:constraintlayout:$versions.constraintlayout"
+ implementation "androidx.core:core-ktx:$versions.core_ktx"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle"
+ implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigation"
+ implementation "androidx.navigation:navigation-ui-ktx:$versions.navigation"
+ implementation "androidx.room:room-ktx:$versions.room"
+ implementation "androidx.room:room-runtime:$versions.room"
+ implementation "com.airbnb.android:lottie:$versions.lottie"
+ implementation "com.google.android.material:material:$versions.material"
+ implementation "com.squareup.picasso:picasso:$versions.picasso"
+ implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit"
+ implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
+ implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttp"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlin_coroutine"
+ implementation "org.koin:koin-android:$versions.koin_android"
+ implementation "org.koin:koin-android-ext:$versions.koin_android"
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+ kapt "androidx.room:room-compiler:$versions.room"
+
+ testImplementation "junit:junit:$versions.junit"
+
+ androidTestImplementation "androidx.test.ext:junit:$versions.ext_junit"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$versions.espresso_core"
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/br/ufpe/cin/vrvs/podcastplayer/ExampleInstrumentedTest.kt b/app/src/androidTest/java/br/ufpe/cin/vrvs/podcastplayer/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..296a46c
--- /dev/null
+++ b/app/src/androidTest/java/br/ufpe/cin/vrvs/podcastplayer/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package br.ufpe.cin.vrvs.podcastplayer
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("br.ufpe.cin.vrvs.podcastplayer", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f5e6bc6
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_podcast_player-playstore.png b/app/src/main/ic_podcast_player-playstore.png
new file mode 100644
index 0000000..b211041
Binary files /dev/null and b/app/src/main/ic_podcast_player-playstore.png differ
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/MainActivity.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/MainActivity.kt
new file mode 100644
index 0000000..3e7d9d5
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/MainActivity.kt
@@ -0,0 +1,23 @@
+package br.ufpe.cin.vrvs.podcastplayer
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.navigation.Navigation
+import androidx.navigation.fragment.NavHostFragment
+import br.ufpe.cin.vrvs.podcastplayer.services.player.PodcastPlayerService.Companion.PODCAST_ID
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ val podcastId = intent.extras?.getString(PODCAST_ID)
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ if (podcastId != null) {
+ val action = NavGraphDirections.actionGlobalPodcastDetailsFragment(podcastId)
+ val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_graph_container) as NavHostFragment
+ val navController = navHostFragment.navController
+ navController.navigate(action)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/app/PodcastPlayerApplication.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/app/PodcastPlayerApplication.kt
new file mode 100644
index 0000000..98554cc
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/app/PodcastPlayerApplication.kt
@@ -0,0 +1,31 @@
+package br.ufpe.cin.vrvs.podcastplayer.app
+
+import android.app.Application
+import br.ufpe.cin.vrvs.podcastplayer.di.apiModule
+import br.ufpe.cin.vrvs.podcastplayer.di.databaseModule
+import br.ufpe.cin.vrvs.podcastplayer.di.preferencesModule
+import br.ufpe.cin.vrvs.podcastplayer.di.repositoryModule
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.startKoin
+
+class PodcastPlayerApplication : Application() {
+
+ private val modules = listOf(
+ apiModule,
+ databaseModule,
+ preferencesModule,
+ repositoryModule
+ )
+
+ override fun onCreate() {
+ super.onCreate()
+
+ // Start Koin
+ startKoin{
+ androidLogger()
+ androidContext(this@PodcastPlayerApplication)
+ modules(modules)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDao.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDao.kt
new file mode 100644
index 0000000..baad78d
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDao.kt
@@ -0,0 +1,57 @@
+package br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.EpisodePersisted
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.EpisodePersistedDownloaded
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.EpisodePersistedPlaying
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.PodcastPersisted
+
+@Dao
+interface PodcastDao {
+
+ @Query("SELECT * FROM podcast_table")
+ suspend fun getPodcasts(): List
+
+ @Query("SELECT * FROM podcast_table where :id = id")
+ suspend fun getPodcast(id: String): PodcastPersisted
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertPodcast(podcast: PodcastPersisted)
+
+ @Query("DELETE FROM podcast_table where :id = id")
+ suspend fun clearPodcast(id: String)
+
+ @Query("SELECT EXISTS(SELECT * FROM podcast_table where :id = id)")
+ fun hasPodcast(id: String): Boolean
+
+ @Query("SELECT * FROM episode_table")
+ suspend fun getEpisodes(): List
+
+ @Query("SELECT * FROM episode_table where :id = id")
+ suspend fun getEpisode(id: String): EpisodePersisted
+
+ @Query("SELECT * FROM episode_table where :podcastId = podcastId order by season, episode")
+ suspend fun getPodcastEpisodes(podcastId: String): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertEpisode(episode: EpisodePersisted)
+
+ @Query("DELETE FROM episode_table where :id = id")
+ suspend fun clearEpisode(id: String)
+
+ @Query("DELETE FROM episode_table where :podcastId = podcastId")
+ suspend fun clearPodcastEpisodes(podcastId: String)
+
+ @Query("SELECT EXISTS(SELECT * FROM episode_table where :id = id)")
+ fun hasEpisode(id: String): Boolean
+
+ @Update(entity = EpisodePersisted::class)
+ suspend fun updateDownloaded(downloaded: EpisodePersistedDownloaded)
+
+ @Update(entity = EpisodePersisted::class)
+ suspend fun updatePlaying(downloaded: EpisodePersistedPlaying)
+}
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDatabase.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDatabase.kt
new file mode 100644
index 0000000..b942879
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/PodcastDatabase.kt
@@ -0,0 +1,20 @@
+package br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.EpisodePersisted
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.PodcastPersisted
+import br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.converter.MapConverter
+
+@Database(
+ entities = [
+ EpisodePersisted::class,
+ PodcastPersisted::class
+ ],
+ version = 1
+)
+@TypeConverters(MapConverter::class)
+abstract class PodcastDatabase : RoomDatabase() {
+ abstract fun podcastDao(): PodcastDao
+}
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/EpisodePersisted.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/EpisodePersisted.kt
new file mode 100644
index 0000000..d4eab81
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/EpisodePersisted.kt
@@ -0,0 +1,81 @@
+package br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.ForeignKey.CASCADE
+import androidx.room.PrimaryKey
+import br.ufpe.cin.vrvs.podcastplayer.data.model.Episode
+
+@Entity(
+ tableName = "episode_table",
+ foreignKeys = [ForeignKey(
+ entity = PodcastPersisted::class,
+ parentColumns = ["id"],
+ childColumns = ["podcastId"],
+ onDelete = CASCADE
+ )]
+)
+data class EpisodePersisted(
+ @PrimaryKey(autoGenerate = false) var id: String,
+ @ColumnInfo(name = "podcastId") var podcastId: String,
+ @ColumnInfo(name = "title") var title: String,
+ @ColumnInfo(name = "description") var description: String,
+ @ColumnInfo(name = "audioUrl") var audioUrl: String,
+ @ColumnInfo(name = "audioType") var audioType: String,
+ @ColumnInfo(name = "imageUrl") var imageUrl: String,
+ @ColumnInfo(name = "datePublished") var datePublished: Long,
+ @ColumnInfo(name = "duration") var duration: Int,
+ @ColumnInfo(name = "episode") var episode: Int,
+ @ColumnInfo(name = "season") var season: Int,
+ @ColumnInfo(name = "downloadId") var downloadId: Long? = null,
+ @ColumnInfo(name = "path") var path: String = "",
+ @ColumnInfo(name = "playing") var playing: Boolean = false
+)
+
+@Entity
+data class EpisodePersistedDownloaded(
+ @ColumnInfo(name = "id") var id: String,
+ @ColumnInfo(name = "downloadId") var downloadId: Long? = null,
+ @ColumnInfo(name = "path") var path: String = ""
+)
+
+@Entity
+data class EpisodePersistedPlaying(
+ @ColumnInfo(name = "id") var id: String,
+ @ColumnInfo(name = "playing") var playing: Boolean = false
+)
+
+fun Episode.Companion.toEpisode(episodePersisted: EpisodePersisted) = Episode(
+ id = episodePersisted.id,
+ podcastId = episodePersisted.podcastId,
+ title = episodePersisted.title,
+ description = episodePersisted.description,
+ audioUrl = episodePersisted.audioUrl,
+ audioType = episodePersisted.audioType,
+ imageUrl = episodePersisted.imageUrl,
+ datePublished = episodePersisted.datePublished,
+ duration = episodePersisted.duration,
+ episode = episodePersisted.episode,
+ season = episodePersisted.season,
+ downloadId = episodePersisted.downloadId,
+ path = episodePersisted.path,
+ playing = episodePersisted.playing
+)
+
+fun Episode.Companion.fromEpisode(episode: Episode) = EpisodePersisted(
+ id = episode.id,
+ podcastId = episode.podcastId,
+ title = episode.title,
+ description = episode.description,
+ audioUrl = episode.audioUrl,
+ audioType = episode.audioType,
+ imageUrl = episode.imageUrl,
+ datePublished = episode.datePublished,
+ duration = episode.duration,
+ episode = episode.episode,
+ season = episode.season,
+ downloadId = episode.downloadId,
+ path = episode.path,
+ playing = episode.playing
+)
\ No newline at end of file
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/PodcastPersisted.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/PodcastPersisted.kt
new file mode 100644
index 0000000..4ec51f0
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/PodcastPersisted.kt
@@ -0,0 +1,38 @@
+package br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import br.ufpe.cin.vrvs.podcastplayer.data.model.Podcast
+import br.ufpe.cin.vrvs.podcastplayer.data.model.Podcast.*
+
+@Entity(tableName = "podcast_table")
+data class PodcastPersisted(
+ @PrimaryKey(autoGenerate = false) var id: String,
+ @ColumnInfo(name = "author") var author: String,
+ @ColumnInfo(name = "description") var description: String,
+ @ColumnInfo(name = "title") var title: String,
+ @ColumnInfo(name = "imageUrl") var imageUrl: String,
+ @ColumnInfo(name = "categories") var categories: Map,
+ @ColumnInfo(name = "subscribed") var subscribed: Boolean = false
+)
+
+fun Companion.toPodcast(podcastPersisted: PodcastPersisted) = Podcast(
+ id = podcastPersisted.id,
+ author = podcastPersisted.author,
+ description = podcastPersisted.description,
+ title = podcastPersisted.title,
+ imageUrl = podcastPersisted.imageUrl,
+ categories = podcastPersisted.categories,
+ subscribed = podcastPersisted.subscribed
+)
+
+fun Companion.fromPodcast(podcast: Podcast) = PodcastPersisted(
+ id = podcast.id,
+ author = podcast.author ?: "",
+ description = podcast.description ?: "",
+ title = podcast.title,
+ imageUrl = podcast.imageUrl,
+ categories = podcast.categories,
+ subscribed = podcast.subscribed
+)
\ No newline at end of file
diff --git a/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/converter/MapConverter.kt b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/converter/MapConverter.kt
new file mode 100644
index 0000000..8a6204c
--- /dev/null
+++ b/app/src/main/java/br/ufpe/cin/vrvs/podcastplayer/data/datasource/local/database/table/converter/MapConverter.kt
@@ -0,0 +1,19 @@
+package br.ufpe.cin.vrvs.podcastplayer.data.datasource.local.database.table.converter
+
+import androidx.room.TypeConverter
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+
+class MapConverter {
+ @TypeConverter
+ fun fromString(value: String): Map {
+ val mapType = object : TypeToken