Skip to content

Commit

Permalink
add network for external camera with retrofit [#35]
Browse files Browse the repository at this point in the history
- 임시로 `http` 사용 가능하도록 설정
- 일부 `data` 모듈에서 사용할 예정인 공용 코드는 `util`에 구현
  • Loading branch information
DokySp committed Oct 8, 2024
1 parent 95bd2bb commit 1c49707
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 1 deletion.
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
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.foke.together.util.retrofit

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

class NetworkCallAdapter<T>(
private val responseType: Type
): CallAdapter<T, Call<Result<T>>> {
override fun responseType(): Type =
responseType

override fun adapt(call: Call<T>): Call<Result<T>> =
// TODO: add NetworkCall for each web-server and external-camera
NetworkCall(call)
}
Loading

0 comments on commit 1c49707

Please sign in to comment.