Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add network for external camera with retrofit [#35] #63

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
android:name=".MainApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" />
android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true" />
<!-- TODO: temporary add usesCleartextTraffic. remove later -->

<!-- TODO: support back-up-->
<!-- android:allowBackup="true"-->
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/foke/together/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class MainApplication: Application() {
}

companion object {
val TAG: String = MainApplication::class.java.simpleName
private val TAG: String = MainApplication::class.java.simpleName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,5 @@ class AppPreferencesSerializer @Inject constructor(): Serializer<AppPreferences>

companion object {
private val TAG = AppPreferencesSerializer::class.java.simpleName

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.foke.together.domain.output

import android.graphics.Bitmap

interface ExternalCameraRepositoryInterface {
fun setCameraSourceUrl(url: String)
suspend fun capture(): Result<Bitmap>
fun getPreviewUrl(): String
}
6 changes: 6 additions & 0 deletions external/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// retrofit
implementation(libs.ok.http.base)
implementation(libs.retrofit.base)
implementation(libs.retrofit.gson)
implementation(libs.google.gson)

// module dependency
implementation(project(":domain"))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.foke.together.external.network

import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Query

interface ExternalCameraApi {
@GET("/capture")
suspend fun capture(
@Query("timeout") timeout: Int? = null
): Result<ResponseBody>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.foke.together.external.network

import com.foke.together.external.network.interceptor.BaseUrlInterceptor
import okhttp3.ResponseBody
import javax.inject.Inject

class ExternalCameraDataSource @Inject constructor(
private val externalCameraApi: ExternalCameraApi,
private val baseUrlInterceptor: BaseUrlInterceptor
){
fun setBaseUrl(url: String) {
baseUrlInterceptor.setBaseUrl(url)
}

suspend fun capture(timeout: Int? = null): Result<ResponseBody> {
return externalCameraApi.capture(timeout)
}

fun getPreviewUrl(): String =
"${baseUrlInterceptor.getBaseUrl()}/preview"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.foke.together.external.network

import com.foke.together.external.network.interceptor.BaseUrlInterceptor
import com.foke.together.util.AppPolicy
import com.foke.together.util.retrofit.NetworkCallAdapterFactory
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Named
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ExternalCameraModule {

@Provides
@Named("cameraIPUrl")
// TODO: need to check in phase3; is it valid injection?
fun provideCameraIPUrl(): String = AppPolicy.EXTERNAL_CAMERA_DEFAULT_SERVER_URL

@Provides
@Singleton
fun provideBaseUrlInterceptor() = BaseUrlInterceptor()

@Singleton
@Provides
fun provideOkHttpClient(
baseUrlInterceptor: BaseUrlInterceptor
) = OkHttpClient.Builder()
.connectTimeout(AppPolicy.EXTERNAL_CAMERA_CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(AppPolicy.EXTERNAL_CAMERA_READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(AppPolicy.EXTERNAL_CAMERA_WRITE_TIMEOUT, TimeUnit.SECONDS)
// .addInterceptor(MockInterceptor()) // TODO: add mock code for test
.addInterceptor(baseUrlInterceptor)
.build()

@Singleton
@Provides
fun provideExternalCameraServerRetrofit(
okHttpClient: OkHttpClient,
@Named("cameraIPUrl") cameraIPUrl: String
) = Retrofit.Builder()
.baseUrl(cameraIPUrl)
.addConverterFactory(GsonConverterFactory.create(
GsonBuilder().setLenient().create()
))
.addCallAdapterFactory(NetworkCallAdapterFactory())
.client(okHttpClient)
.build()

@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ExternalCameraApi =
retrofit.create(ExternalCameraApi::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.foke.together.external.network.interceptor

import com.foke.together.util.AppLog
import com.foke.together.util.AppPolicy
import okhttp3.Interceptor
import okhttp3.Response

class BaseUrlInterceptor: Interceptor {
@Volatile
private var baseUrl: String? = null
@Volatile
private var scheme: String? = null
@Volatile
private var host: String? = null
@Volatile
private var port: Int? = null

fun getBaseUrl() = baseUrl ?: AppPolicy.EXTERNAL_CAMERA_DEFAULT_SERVER_URL

fun setBaseUrl(newBaseUrl: String) {
baseUrl = newBaseUrl
try {
val sep = newBaseUrl.indexOf("://")
scheme = newBaseUrl.substring(0, sep)
val newHost = newBaseUrl.substring(sep + 3, newBaseUrl.length)

val pSep = newHost.lastIndexOf(":")
port = if (pSep != -1) {
host = newHost.substring(0, pSep)
newHost.substring(pSep + 1, newHost.length).toInt()
} else {
host = newHost
null
}
} catch (e: Exception) {
AppLog.e(TAG, "setBaseUrl", "cannot parse newBaseUrl")
}
}

override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
scheme?.let { s ->
host?.let { h ->
val newUrl = request.url.newBuilder().run {
scheme(s)
host(h)
port?.let { port(it) }
build()
}
request = request.newBuilder()
.url(newUrl)
.build()
}
}
return chain.proceed(request)
}

companion object {
private val TAG: String = BaseUrlInterceptor::class.java.simpleName
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.foke.together.external.network.interceptor

import okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.Response

class MockInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// TODO: make mock for test
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_2)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.foke.together.external.repository

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.foke.together.domain.output.ExternalCameraRepositoryInterface
import com.foke.together.external.network.ExternalCameraDataSource
import javax.inject.Inject

class ExternalCameraRepository @Inject constructor(
private val externalCameraDataSource: ExternalCameraDataSource
): ExternalCameraRepositoryInterface {
override fun setCameraSourceUrl(url: String) {
externalCameraDataSource.setBaseUrl(url)
}

override suspend fun capture(): Result<Bitmap> {
return externalCameraDataSource.capture()
.map { responseBody ->
BitmapFactory.decodeStream(responseBody.byteStream())
}
}

override fun getPreviewUrl() =
externalCameraDataSource.getPreviewUrl()

companion object {
private val TAG = ExternalCameraRepository::class.java.simpleName
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.foke.together.external.repository.di

import com.foke.together.domain.output.ExternalCameraRepositoryInterface
import com.foke.together.external.repository.ExternalCameraRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent

@Module
@InstallIn(ViewModelComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindAppPreferenceRepository(
externalCameraRepository: ExternalCameraRepository
): ExternalCameraRepositoryInterface
}
6 changes: 6 additions & 0 deletions util/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// retrofit
implementation(libs.ok.http.base)
implementation(libs.retrofit.base)
implementation(libs.retrofit.gson)
implementation(libs.google.gson)

// module dependency
implementation(project(":entity"))
}
8 changes: 8 additions & 0 deletions util/src/main/java/com/foke/together/util/AppPolicy.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.foke.together.util

object AppPolicy {
const val isDebugMode = true

const val DEFAULT_EXTERNAL_CAMERA_IP = "0.0.0.0"

// network
const val EXTERNAL_CAMERA_DEFAULT_SERVER_URL = "http://0.0.0.0"
const val EXTERNAL_CAMERA_CONNECT_TIMEOUT = 10L
const val EXTERNAL_CAMERA_READ_TIMEOUT = 10L
const val EXTERNAL_CAMERA_WRITE_TIMEOUT = 10L
}
81 changes: 81 additions & 0 deletions util/src/main/java/com/foke/together/util/retrofit/NetworkCall.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.foke.together.util.retrofit

import com.foke.together.util.AppLog
import com.foke.together.util.AppPolicy
import okhttp3.Request
import okio.IOException
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class NetworkCall<T>(
private val call: Call<T>,
): Call<Result<T>> {
override fun enqueue(callback: Callback<Result<T>>) {
call.enqueue(object: Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (AppPolicy.isDebugMode) {
AppLog.e(TAG, "onResponse", "success: $response")
}

if (response.isSuccessful) {
// success
callback.onResponse(
this@NetworkCall,
Response.success(Result.success(response.body()!!))
)
} else {
// error
callback.onResponse(
this@NetworkCall,
Response.success(
// TODO: change to custom `error type`
Result.failure(Exception())
)
)
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
if (AppPolicy.isDebugMode) {
AppLog.e(TAG, "onResponse", "failure: $t")
}

when(t) {
// TODO: define each error cases by `error type`
is IOException -> Result.failure<T>(t)
else -> Result.failure<T>(t)
}.also {
callback.onResponse(this@NetworkCall, Response.success(it))
}
}
})
}

override fun clone(): Call<Result<T>> =
NetworkCall(call.clone())

override fun execute(): Response<Result<T>> =
// TODO: check in case of null body
Response.success(Result.success(call.execute().body()!!))

override fun isExecuted(): Boolean =
call.isExecuted

override fun cancel() =
call.cancel()

override fun isCanceled(): Boolean =
call.isCanceled

override fun request(): Request =
call.request()

override fun timeout(): Timeout =
call.timeout()

companion object {
private val TAG: String = NetworkCall::class.java.simpleName
}
}
Loading