Skip to content

Commit

Permalink
Upgrades! Makes it work with android SDK version 33 (#109)
Browse files Browse the repository at this point in the history
* Dependency updates, build the app on API 33 (credit to @dkowis)

* Collections MVP Implementation

 - Fetch collections during library refreshes
 - Add bottom nav menu entry for collections (if library has collections)
 - Implement a screen to show all collections
 - Implement very basic "collection details" screen listing the items in a collection. Not currently using library sorting! Not showing any details
 - Probably plenty of bugs re: weird data types, but simple implementation working

* Update AGP to 8.0.0

* Re-enable silent audio, weaken sensor requirements

---------

Co-authored-by: Matt Vaughn <[email protected]>
  • Loading branch information
dkowis and mattttvaughn authored Apr 23, 2023
1 parent ff318fc commit b4dc495
Show file tree
Hide file tree
Showing 63 changed files with 1,974 additions and 268 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ captures/
bin/
gen/
target/
app/freeAsInBeer
app/googlePlay
app/release

# android studio files
.idea/
Expand Down
41 changes: 22 additions & 19 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "kotlin-kapt"
apply plugin: "kotlin-parcelize"
apply plugin: "com.google.android.gms.oss-licenses-plugin"

android {
compileSdkVersion 31
compileSdkVersion 33
defaultConfig {
applicationId "io.github.mattpvaughn.chronicle"
minSdkVersion 21
targetSdkVersion 30
versionCode 24
versionName "0.52.1"
minSdkVersion 31
targetSdkVersion 33
versionCode 26
versionName '0.54.0'
testInstrumentationRunner "io.github.mattpvaughn.chronicle.application.ChronicleTestRunner"
}
buildTypes {
Expand All @@ -35,11 +35,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
// Shared code b/w test and androidTest: mocks "n" stuff
sourceSets {
Expand All @@ -58,14 +58,11 @@ android {
arg("room.expandProjection", "true")
}
}
lintOptions {
abortOnError false
}
kotlinOptions {
freeCompilerArgs = ["-Xallow-result-return-type"]
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
freeCompilerArgs += ["-Xopt-in=kotlin.time.ExperimentalTime"]
freeCompilerArgs += ["-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"]
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
freeCompilerArgs += ["-opt-in=kotlin.time.ExperimentalTime"]
freeCompilerArgs += ["-opt-in=kotlinx.coroutines.InternalCoroutinesApi"]
}
flavorDimensions "freeAsInBeer"
productFlavors {
Expand All @@ -78,8 +75,14 @@ android {
buildConfigField "boolean", "FREE_AS_IN_BEER", "false"
}
}
lint {
abortOnError false
}
namespace 'io.github.mattpvaughn.chronicle'
}



dependencies {
implementation fileTree(include: ["*.jar"], dir: "libs")

Expand All @@ -90,7 +93,7 @@ dependencies {
implementation "com.google.android.material:material:$materialLibVersion"
implementation "androidx.appcompat:appcompat:$supportlibVersion"
implementation "androidx.fragment:fragment-ktx:$appCompatFragmentVersion"
implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"

// AndroidX
Expand Down Expand Up @@ -150,7 +153,7 @@ dependencies {


// Fresco - image loading
implementation 'com.facebook.fresco:fresco:2.4.0'
implementation 'com.facebook.fresco:fresco:2.6.0'
implementation "com.facebook.fresco:imagepipeline-okhttp3:2.4.0"

// LocalBroadcastManager
Expand All @@ -162,10 +165,10 @@ dependencies {
implementation "com.google.android.exoplayer:extension-mediasession:$exoplayerVersion"

// ExoPlayer extensions for FLAC and OPUS file types
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:$exoplayerVersion") {
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:$exoplayerExtensions") {
transitive = false
}
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:$exoplayerVersion"){
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:$exoplayerExtensions"){
transitive = false
}

Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.github.mattpvaughn.chronicle">
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,21 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
}

binding.bottomNav.setOnNavigationItemSelectedListener {
// TODO: show/hide this item on launch more performantly
viewModel.hasCollections.observe(this) {
binding.bottomNav.menu.findItem(R.id.nav_collections).isVisible = it
}

binding.bottomNav.setOnItemSelectedListener {
when (it.itemId) {
R.id.nav_settings -> navigator.showSettings()
R.id.nav_library -> navigator.showLibrary()
R.id.nav_collections -> navigator.showCollections()
R.id.nav_home -> navigator.showHome()
else -> throw NoWhenBranchMatchedException("Unknown bottom tab id: ${it.itemId}")
}
viewModel.minimizeCurrentlyPlaying()
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}

if (savedInstanceState == null) {
Expand Down Expand Up @@ -174,8 +180,8 @@ class MainActivity : AppCompatActivity() {
this,
object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.support.v4.media.session.PlaybackStateCompat.STATE_NONE
import android.support.v4.media.session.PlaybackStateCompat.STATE_STOPPED
import androidx.lifecycle.*
import io.github.mattpvaughn.chronicle.application.MainActivityViewModel.BottomSheetState.*
import io.github.mattpvaughn.chronicle.data.local.CollectionsRepository
import io.github.mattpvaughn.chronicle.data.local.IBookRepository
import io.github.mattpvaughn.chronicle.data.local.ITrackRepository
import io.github.mattpvaughn.chronicle.data.model.*
Expand All @@ -27,6 +28,7 @@ class MainActivityViewModel(
private val trackRepository: ITrackRepository,
private val bookRepository: IBookRepository,
private val mediaServiceConnection: MediaServiceConnection,
collectionsRepository: CollectionsRepository
) : ViewModel(), MainActivity.CurrentlyPlayingInterface {

@Suppress("UNCHECKED_CAST")
Expand All @@ -35,15 +37,17 @@ class MainActivityViewModel(
private val trackRepository: ITrackRepository,
private val bookRepository: IBookRepository,
private val mediaServiceConnection: MediaServiceConnection,
private val collectionsRepository: CollectionsRepository
) : ViewModelProvider.Factory {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) {
return MainActivityViewModel(
loginRepo,
trackRepository,
bookRepository,
mediaServiceConnection
mediaServiceConnection,
collectionsRepository
) as T
} else {
throw IllegalArgumentException("Cannot instantiate $modelClass from MainActivityViewModel.Factory")
Expand All @@ -58,7 +62,7 @@ class MainActivityViewModel(
EXPANDED
}

val isLoggedIn = Transformations.map(loginRepo.loginEvent) {
val isLoggedIn = loginRepo.loginEvent.map {
it.peekContent() == LOGGED_IN_FULLY
}

Expand All @@ -72,7 +76,7 @@ class MainActivityViewModel(
bookRepository.getAudiobookAsync(id) ?: EMPTY_AUDIOBOOK
}

private var tracks = Transformations.switchMap(audiobookId) { id ->
private var tracks = audiobookId.switchMap { id ->
if (id != NO_AUDIOBOOK_FOUND_ID) {
trackRepository.getTracksForAudiobook(id)
} else {
Expand All @@ -84,6 +88,8 @@ class MainActivityViewModel(
val errorMessage: LiveData<Event<String>>
get() = _errorMessage

val hasCollections = collectionsRepository.hasCollections()

// Used to cache tracks.asChapterList when tracks changes
private val tracksAsChaptersCache = mapAsync(tracks, viewModelScope) {
it.asChapterList()
Expand Down Expand Up @@ -111,7 +117,7 @@ class MainActivityViewModel(
}.getChapterAt(_tracks.getActiveTrack().id.toLong(), currentTrackProgress).title
}

val isPlaying = Transformations.map(mediaServiceConnection.playbackState) {
val isPlaying = mediaServiceConnection.playbackState.map {
it.isPlaying
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fun getBookDatabase(context: Context): BookDatabase {
BOOK_MIGRATION_4_5,
BOOK_MIGRATION_5_6,
BOOK_MIGRATION_6_7,
BOOK_MIGRATION_7_8
BOOK_MIGRATION_7_8,
).build()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.mattpvaughn.chronicle.data.local

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.room.*
import io.github.mattpvaughn.chronicle.data.model.Collection

private const val COLLECTIONS_DATABASE_NAME = "collections_db"

private lateinit var INSTANCE: CollectionsDatabase
fun getCollectionsDatabase(context: Context): CollectionsDatabase {
synchronized(CollectionsDatabase::class.java) {
if (!::INSTANCE.isInitialized) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
CollectionsDatabase::class.java,
COLLECTIONS_DATABASE_NAME
).addMigrations().build()
}
}
return INSTANCE
}

@Database(entities = [Collection::class], version = 1, exportSchema = false)
abstract class CollectionsDatabase : RoomDatabase() {
abstract val collectionsDao: CollectionsDao
}

@Dao
interface CollectionsDao {
@Query("SELECT * FROM Collection ORDER BY title")
fun getAllRows(): LiveData<List<Collection>>

@Query("SELECT * FROM Collection WHERE id = :id LIMIT 1")
fun getCollection(id: Int): LiveData<Collection?>

@Query("SELECT * FROM Collection WHERE :collectionId = id")
suspend fun getCollectionAsync(collectionId: Int): Collection

@Query("SELECT * FROM Collection")
fun getCollections(): List<Collection>

@Query("SELECT Count(id) FROM Collection")
fun countCollections(): LiveData<Long>

@Query("DELETE FROM Collection")
suspend fun clear()

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(rows: List<Collection>)

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun update(collection: Collection)

@Query("DELETE FROM Collection WHERE id IN (:collectionsToRemove)")
fun removeAll(collectionsToRemove: List<Long>): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.github.mattpvaughn.chronicle.data.local

import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import io.github.mattpvaughn.chronicle.data.model.Collection
import io.github.mattpvaughn.chronicle.data.sources.plex.PlexMediaService
import io.github.mattpvaughn.chronicle.data.sources.plex.PlexPrefsRepo
import io.github.mattpvaughn.chronicle.data.sources.plex.model.asAudiobooks
import io.github.mattpvaughn.chronicle.data.sources.plex.model.asCollections
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CollectionsRepository @Inject constructor(
private val plexMediaService: PlexMediaService,
private val prefsRepo: PrefsRepo,
private val plexPrefsRepo: PlexPrefsRepo,
private val collectionsDao: CollectionsDao
) {

// TODO: handle collections sorting!
suspend fun getChildIds(collectionId: Int): List<Long> {
return collectionsDao.getCollectionAsync(collectionId).childIds
}

fun getCollection(id: Int): LiveData<Collection?> = collectionsDao.getCollection(id)

fun getAllCollections(): LiveData<List<Collection>> = collectionsDao.getAllRows()

fun hasCollections(): LiveData<Boolean> = collectionsDao
.countCollections()
.map { it > 0 }

suspend fun refreshCollectionsPaginated() {
prefsRepo.lastRefreshTimeStamp = System.currentTimeMillis()
val networkCollections: MutableList<Collection> = mutableListOf()
withContext(Dispatchers.IO) {
try {
val libraryId = plexPrefsRepo.library?.id ?: return@withContext
var chaptersLeft = 1L
// Maximum number of pages of data we fetch. Failsafe in case of bad data from the
// server since we don't want infinite loops. This limits us to a maximum 1,000,000
// collections for now
val maxIterations = 5000
var i = 0
while (chaptersLeft > 0 && i < maxIterations) {
val response = plexMediaService
.retrieveCollectionsPaginated(libraryId, i * 100)
.plexMediaContainer
chaptersLeft = response.totalSize - (response.offset + response.size)
networkCollections.addAll(response.asCollections())
i++
}
} catch (t: Throwable) {
Timber.i("Failed to retrieve books: $t")
}
}

withContext(Dispatchers.IO) {
try {
val collectionsWithChildIds = networkCollections.map {
val collectionItems = plexMediaService.fetchBooksInCollection(it.id)
.plexMediaContainer
.asAudiobooks()

val childIds = collectionItems.map { book -> book.id.toLong() }
it.copy(childIds = childIds)
}
collectionsDao.insertAll(collectionsWithChildIds)
} catch (t: Throwable) {
Timber.i("Failed to retrieve books: $t")
}
}
}

suspend fun clear() {
collectionsDao.clear()
}
}
Loading

0 comments on commit b4dc495

Please sign in to comment.