Skip to content

Commit 47d3f83

Browse files
committed
prepare movie support
1 parent 218dca7 commit 47d3f83

13 files changed

Lines changed: 321 additions & 18 deletions

File tree

composeApp/src/commonMain/kotlin/dev/datlag/mimasu/ui/navigation/detail/movie/MovieDetail.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.datlag.mimasu.ui.navigation.detail.movie
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.fillMaxSize
56
import androidx.compose.foundation.layout.fillMaxWidth
@@ -11,6 +12,7 @@ import androidx.compose.material3.LinearProgressIndicator
1112
import androidx.compose.material3.Scaffold
1213
import androidx.compose.material3.SnackbarHost
1314
import androidx.compose.material3.SnackbarHostState
15+
import androidx.compose.material3.Text
1416
import androidx.compose.material3.TopAppBarDefaults
1517
import androidx.compose.material3.rememberTopAppBarState
1618
import androidx.compose.runtime.Composable
@@ -21,13 +23,16 @@ import androidx.compose.ui.ExperimentalComposeUiApi
2123
import androidx.compose.ui.Modifier
2224
import androidx.compose.ui.backhandler.BackHandler
2325
import androidx.compose.ui.draw.clip
26+
import androidx.compose.ui.graphics.Color
2427
import androidx.compose.ui.input.nestedscroll.nestedScroll
2528
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2629
import dev.chrisbanes.haze.HazeState
2730
import dev.datlag.mimasu.tmdb.model.details.Movie
2831
import dev.datlag.mimasu.ui.custom.ErrorState
2932
import dev.datlag.mimasu.ui.navigation.detail.movie.components.MovieToolbar
3033
import dev.datlag.mimasu.ui.navigation.detail.movie.components.MovieWatchProviderFAB
34+
import dev.datlag.mimasu.ui.other.MovieState
35+
import dev.datlag.mimasu.ui.other.rememberMovieAvailability
3136
import dev.datlag.mimasu.ui.viewmodel.MovieViewModel
3237
import dev.datlag.mimasu.ui.viewmodel.accountViewModel
3338
import dev.datlag.mimasu.ui.viewmodel.kodeinViewModel
@@ -53,6 +58,10 @@ fun MovieDetail(
5358
val haze = remember { HazeState() }
5459
val listState = rememberLazyListState()
5560
val snackbarState = remember { SnackbarHostState() }
61+
val movieAvailability = rememberMovieAvailability(
62+
movie = movieState.getOrNull(),
63+
initial = initial
64+
)
5665

5766
BackHandler(enabled = true) {
5867
onBack()
@@ -77,10 +86,22 @@ fun MovieDetail(
7786
)
7887
},
7988
floatingActionButton = {
80-
MovieWatchProviderFAB(
81-
movie = movieState.getOrNull(),
82-
onWatchClick = onWatchClick
83-
)
89+
when (movieAvailability) {
90+
is MovieState.Available -> {
91+
if (movieAvailability.state) {
92+
Text("Movie Available", modifier = Modifier.background(Color.Red), color = Color.White)
93+
} else {
94+
MovieWatchProviderFAB(
95+
movie = movieState.getOrNull(),
96+
onWatchClick = onWatchClick
97+
)
98+
}
99+
}
100+
else -> MovieWatchProviderFAB(
101+
movie = movieState.getOrNull(),
102+
onWatchClick = onWatchClick
103+
)
104+
}
84105
},
85106
snackbarHost = {
86107
SnackbarHost(hostState = snackbarState)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.datlag.mimasu.extension;
2+
3+
import dev.datlag.mimasu.extension.movie.MovieCallback;
4+
5+
interface IMovieProvider {
6+
const int VERSION = 1;
7+
8+
void requestMovieId(in byte[] request, in MovieCallback callback);
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package dev.datlag.mimasu.extension.movie;
2+
3+
interface MovieCallback {
4+
void onResult(in int id, in boolean available);
5+
}

extension/src/androidMain/kotlin/dev/datlag/mimasu/extension/ExtensionInitializer.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ class ExtensionInitializer : Initializer<ExtensionInitializer.State> {
1919
GlobalScope.launch {
2020
it.initialize()
2121
}
22+
},
23+
movieProviderAndroid = MovieProviderAndroid(context).also {
24+
GlobalScope.launch {
25+
it.initialize()
26+
}
2227
}
2328
).also { result ->
2429
state.update { result }
@@ -31,7 +36,8 @@ class ExtensionInitializer : Initializer<ExtensionInitializer.State> {
3136

3237
data class State(
3338
val updateProvider: UpdateProviderAndroid,
34-
val showProviderAndroid: ShowProviderAndroid
39+
val showProviderAndroid: ShowProviderAndroid,
40+
val movieProviderAndroid: MovieProviderAndroid
3541
)
3642

3743
companion object {
@@ -51,27 +57,40 @@ class ExtensionInitializer : Initializer<ExtensionInitializer.State> {
5157
.showProviderAndroid
5258
}
5359

60+
fun getMovieProvider(context: Context): MovieProviderAndroid {
61+
return state.value?.movieProviderAndroid ?: androidx.startup.AppInitializer
62+
.getInstance(context)
63+
.initializeComponent(ExtensionInitializer::class.java)
64+
.movieProviderAndroid
65+
}
66+
5467
private fun nullableUpdateProvider(): UpdateProviderAndroid? = state.value?.updateProvider
5568

5669
private fun nullableShowProvider(): ShowProviderAndroid? = state.value?.showProviderAndroid
5770

71+
private fun nullableMovieProvider(): MovieProviderAndroid? = state.value?.movieProviderAndroid
72+
5873
fun unbindAll(context: Context) {
5974
nullableUpdateProvider()?.unbind(context)
6075
nullableShowProvider()?.unbind(context)
76+
nullableMovieProvider()?.unbind(context)
6177
}
6278

6379
suspend fun initialize(context: Context) {
6480
getShowProvider(context).initialize()
81+
getMovieProvider(context).initialize()
6582
}
6683

6784
suspend fun rebindAll(context: Context) {
6885
nullableUpdateProvider()?.rebind(context)
6986
nullableShowProvider()?.rebind(context)
87+
nullableMovieProvider()?.rebind(context)
7088
}
7189

7290
suspend fun rebindIfNoneAvailable(context: Context) {
7391
nullableUpdateProvider()?.rebindIfUnavailable(context)
7492
nullableShowProvider()?.rebindIfNoneAvailable(context)
93+
nullableMovieProvider()?.rebindIfNoneAvailable(context)
7594
}
7695

7796
fun rebindIfNoneAvailable(scope: CoroutineScope, context: Context) = scope.launch {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package dev.datlag.mimasu.extension
2+
3+
import android.content.Context
4+
import dev.datlag.mimasu.core.Virtual
5+
import dev.datlag.mimasu.extension.model.Movie
6+
import dev.datlag.mimasu.extension.service.MovieService
7+
import dev.datlag.tooling.async.suspendCatching
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.async
10+
import kotlinx.coroutines.awaitAll
11+
import kotlinx.coroutines.coroutineScope
12+
import kotlinx.coroutines.withContext
13+
14+
class MovieProviderAndroid(private val context: Context) : MovieProvider {
15+
16+
private var extensionPackages = emptySet<String>()
17+
private var services = bind(context)
18+
private val boundServices: List<MovieService>
19+
get() = services.filter { it.isBound }
20+
21+
override suspend fun initialize() {
22+
if (extensionPackages.isEmpty()) {
23+
extensionPackages = withContext(Dispatchers.IO) {
24+
AIDLService.extensions(
25+
packageManager = context.packageManager,
26+
action = MovieService.ACTION
27+
)
28+
}
29+
}
30+
}
31+
32+
fun unbind(context: Context): Boolean {
33+
return boundServices.map {
34+
AIDLService.unbind(context, it)
35+
}.all { it }
36+
}
37+
38+
private fun bind(context: Context) = extensionPackages.map { packageName ->
39+
MovieService(context).also { service ->
40+
AIDLService.bind(context, service, packageName)
41+
}
42+
}
43+
44+
suspend fun rebind(context: Context) {
45+
withContext(Dispatchers.Main) {
46+
unbind(context)
47+
}
48+
49+
extensionPackages = withContext(Dispatchers.Virtual ?: Dispatchers.IO) {
50+
AIDLService.extensions(
51+
packageManager = context.packageManager,
52+
action = MovieService.ACTION
53+
)
54+
}
55+
services = withContext(Dispatchers.Main) {
56+
bind(context)
57+
}
58+
}
59+
60+
suspend fun rebindIfNoneAvailable(context: Context) {
61+
if (boundServices.isEmpty()) {
62+
rebind(context)
63+
}
64+
}
65+
66+
override suspend fun requestId(request: Movie.Request): Boolean = coroutineScope {
67+
val bytes = request.toByteArray()
68+
69+
return@coroutineScope boundServices.map { async {
70+
suspendCatching {
71+
it.requestId(request.tmdbId, bytes)
72+
}.getOrNull()
73+
} }.awaitAll().filterNotNull().any { it }
74+
}
75+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package dev.datlag.mimasu.extension.service
2+
3+
import android.content.Context
4+
import android.os.IBinder
5+
import dev.datlag.mimasu.extension.AIDLService
6+
import dev.datlag.mimasu.extension.IMovieProvider
7+
import dev.datlag.mimasu.extension.movie.MovieCallback
8+
import dev.datlag.tooling.scopeCatching
9+
import kotlinx.coroutines.suspendCancellableCoroutine
10+
import java.util.concurrent.ConcurrentHashMap
11+
12+
internal class MovieService(context: Context) : AIDLService<IMovieProvider>(context) {
13+
override val connectionAction: String = ACTION
14+
15+
private val mappedIds = ConcurrentHashMap<Int, Int>()
16+
17+
override fun bind(service: IBinder?): IMovieProvider? {
18+
return scopeCatching {
19+
IMovieProvider.Stub.asInterface(service)
20+
}.getOrNull()
21+
}
22+
23+
override fun onConnected(service: IMovieProvider) { }
24+
25+
override fun onDisconnected() { }
26+
27+
suspend fun requestId(tmdbId: Int?, bytes: ByteArray): Boolean = suspendCancellableCoroutine { continuation ->
28+
if (!isBound) {
29+
continuation.cancel()
30+
return@suspendCancellableCoroutine
31+
}
32+
33+
val connection = service ?: run {
34+
continuation.cancel()
35+
return@suspendCancellableCoroutine
36+
}
37+
38+
connection.requestMovieId(bytes, object : MovieCallback.Stub() {
39+
override fun onResult(id: Int, available: Boolean) {
40+
if (tmdbId != null) {
41+
mappedIds[tmdbId] = id
42+
}
43+
44+
continuation.resumeWith(Result.success(tmdbId != null && available))
45+
}
46+
})
47+
}
48+
49+
companion object {
50+
internal const val ACTION = "dev.datlag.mimasu.extension.IMovieProvider"
51+
}
52+
}

extension/src/androidMain/kotlin/dev/datlag/mimasu/extension/service/ShowService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import dev.datlag.mimasu.extension.show.StreamCallback
1111
import dev.datlag.tooling.scopeCatching
1212
import kotlinx.atomicfu.atomic
1313
import kotlinx.coroutines.suspendCancellableCoroutine
14+
import java.util.concurrent.ConcurrentHashMap
1415

1516
internal class ShowService(context: Context) : AIDLService<IShowInfoProvider>(context) {
1617
override val connectionAction: String = ACTION
1718

18-
private val mappedIds by atomic<MutableMap<Int, Int>>(mutableMapOf())
19+
private val mappedIds = ConcurrentHashMap<Int, Int>()
1920

2021
override fun bind(service: IBinder?): IShowInfoProvider? {
2122
return scopeCatching {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.datlag.mimasu.extension
2+
3+
import dev.datlag.mimasu.extension.model.Movie
4+
5+
interface MovieProvider {
6+
7+
suspend fun initialize()
8+
suspend fun requestId(request: Movie.Request): Boolean
9+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.datlag.mimasu.extension.model
2+
3+
import kotlinx.serialization.ExperimentalSerializationApi
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.encodeToByteArray
6+
import kotlinx.serialization.protobuf.ProtoBuf
7+
8+
@Serializable
9+
sealed interface Movie {
10+
11+
@Serializable
12+
data class Request(
13+
val tmdbId: Int? = null,
14+
val imdbId: String? = null,
15+
val wikidataId: String? = null,
16+
val title: String? = null,
17+
val originalTitle: String? = null,
18+
val firstReleaseYear: Int? = null,
19+
val isAnimation: Boolean? = null,
20+
val appLocale: String? = null
21+
) : Movie {
22+
23+
@OptIn(ExperimentalSerializationApi::class)
24+
fun toByteArray(): ByteArray {
25+
return protobuf.encodeToByteArray(this)
26+
}
27+
}
28+
29+
companion object {
30+
@OptIn(ExperimentalSerializationApi::class)
31+
private val protobuf = ProtoBuf {
32+
encodeDefaults = false
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)