diff --git a/data/src/main/java/com/foke/together/data/repository/AppPreferencesRepository.kt b/data/src/main/java/com/foke/together/data/repository/AppPreferencesRepository.kt index 4907d7f..56db828 100644 --- a/data/src/main/java/com/foke/together/data/repository/AppPreferencesRepository.kt +++ b/data/src/main/java/com/foke/together/data/repository/AppPreferencesRepository.kt @@ -3,7 +3,9 @@ package com.foke.together.data.repository import androidx.datastore.core.DataStore import com.foke.together.AppPreferences import com.foke.together.CameraSource +import com.foke.together.CutFrameSource import com.foke.together.domain.interactor.entity.CameraSourceType +import com.foke.together.domain.interactor.entity.CutFrameSourceType import com.foke.together.domain.interactor.entity.ExternalCameraIP import com.foke.together.domain.output.AppPreferenceInterface import kotlinx.coroutines.flow.Flow @@ -58,4 +60,30 @@ class AppPreferencesRepository @Inject constructor( it.toBuilder().clear().build() } } + + override fun getCutFrameSourceType(): Flow = + appPreferencesFlow.map { + it.cutFrameSource?.run { + when (this) { + CutFrameSource.MAKER_FAIRE -> CutFrameSourceType.MAKER_FAIRE + CutFrameSource.FOKE_LIGHT -> CutFrameSourceType.FOKE_LIGHT + CutFrameSource.FOKE_DARK -> CutFrameSourceType.FOKE_DARK + CutFrameSource.UNRECOGNIZED -> null + } + } ?: CutFrameSourceType.MAKER_FAIRE // set to default INTERNAL + } + + override suspend fun setCutFrameSourceType(type: CutFrameSourceType){ + when (type) { + CutFrameSourceType.MAKER_FAIRE -> CutFrameSource.MAKER_FAIRE + CutFrameSourceType.FOKE_LIGHT -> CutFrameSource.FOKE_LIGHT + CutFrameSourceType.FOKE_DARK -> CutFrameSource.FOKE_DARK + }.apply { + appPreferences.updateData { + it.toBuilder() + .setCutFrameSource(this) + .build() + } + } + } } \ No newline at end of file diff --git a/data/src/main/proto/app_preferences.proto b/data/src/main/proto/app_preferences.proto index 8243404..fe8feb9 100644 --- a/data/src/main/proto/app_preferences.proto +++ b/data/src/main/proto/app_preferences.proto @@ -14,6 +14,7 @@ message AppPreferences { CameraSource camera_source = 10; string external_camera_ip = 11; + CutFrameSource cut_frame_source = 12; // TODO: sample code. remove later. string sample_id = 999997; @@ -24,4 +25,10 @@ message AppPreferences { enum CameraSource { CAMERA_SOURCE_INTERNAL = 0; CAMERA_SOURCE_EXTERNAL = 1; +} + +enum CutFrameSource { + MAKER_FAIRE = 0; + FOKE_LIGHT = 1; + FOKE_DARK = 2; } \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GetCutFrameSourceTypeUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GetCutFrameSourceTypeUseCase.kt new file mode 100644 index 0000000..cd18835 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/GetCutFrameSourceTypeUseCase.kt @@ -0,0 +1,14 @@ +package com.foke.together.domain.interactor + +import com.foke.together.domain.interactor.entity.CutFrameSourceType +import com.foke.together.domain.output.AppPreferenceInterface +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class GetCutFrameSourceTypeUseCase @Inject constructor( + private val appPreference: AppPreferenceInterface +) { + operator fun invoke(): Flow = + appPreference.getCutFrameSourceType().map { it } +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/SetCutFrameSourceTypeUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/SetCutFrameSourceTypeUseCase.kt new file mode 100644 index 0000000..a22df09 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/SetCutFrameSourceTypeUseCase.kt @@ -0,0 +1,13 @@ +package com.foke.together.domain.interactor + +import com.foke.together.domain.interactor.entity.CameraSourceType +import com.foke.together.domain.interactor.entity.CutFrameSourceType +import com.foke.together.domain.output.AppPreferenceInterface +import javax.inject.Inject + +class SetCutFrameSourceTypeUseCase @Inject constructor( + private val appPreference: AppPreferenceInterface +) { + suspend operator fun invoke(cutFrameSourceType: CutFrameSourceType) = + appPreference.setCutFrameSourceType(cutFrameSourceType) +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameSourceType.kt b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameSourceType.kt new file mode 100644 index 0000000..28cf116 --- /dev/null +++ b/domain/src/main/java/com/foke/together/domain/interactor/entity/CutFrameSourceType.kt @@ -0,0 +1,7 @@ +package com.foke.together.domain.interactor.entity + +enum class CutFrameSourceType { + MAKER_FAIRE, + FOKE_LIGHT, + FOKE_DARK, +} \ No newline at end of file diff --git a/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt b/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt index fc924bf..1d8b56c 100644 --- a/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt +++ b/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt @@ -1,6 +1,7 @@ package com.foke.together.domain.output import com.foke.together.domain.interactor.entity.CameraSourceType +import com.foke.together.domain.interactor.entity.CutFrameSourceType import com.foke.together.domain.interactor.entity.ExternalCameraIP import kotlinx.coroutines.flow.Flow @@ -11,5 +12,8 @@ interface AppPreferenceInterface { fun getExternalCameraIP(): Flow suspend fun setExternalCameraIP(ip: ExternalCameraIP) + fun getCutFrameSourceType(): Flow + suspend fun setCutFrameSourceType(type: CutFrameSourceType) + suspend fun clearAll() } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec002ee..8bf78d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,6 +59,8 @@ protoc = "com.google.protobuf:protoc:4.28.2" constraint-layout-compose = "1.0.1" android-mjpeg-view = "1.1.3" material-icons-extended = "1.7.2" +ui-graphics-android = "1.7.3" +coil-compose = "2.7.0" # test ----------- junit = "4.13.2" @@ -120,6 +122,8 @@ protobuf_javalite = { group = "com.google.protobuf", name = "protobuf-javalite", androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraint-layout-compose" } androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "material-icons-extended" } android-mjpeg-view = { group = "com.perthcpe23.dev", name = "android-mjpeg-view", version.ref = "android-mjpeg-view" } +androidx-ui-graphics-android = { group = "androidx.compose.ui", name = "ui-graphics-android", version.ref = "ui-graphics-android" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" } # test ----------- junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/presenter/build.gradle.kts b/presenter/build.gradle.kts index 521f97d..142fb79 100644 --- a/presenter/build.gradle.kts +++ b/presenter/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { // navigation implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.navigation.compose) + implementation(libs.coil.compose) // test testImplementation(libs.junit) diff --git a/presenter/src/main/java/com/foke/together/presenter/frame/MakerFaireFrame.kt b/presenter/src/main/java/com/foke/together/presenter/frame/MakerFaireFrame.kt index 994ba73..6982ee4 100644 --- a/presenter/src/main/java/com/foke/together/presenter/frame/MakerFaireFrame.kt +++ b/presenter/src/main/java/com/foke/together/presenter/frame/MakerFaireFrame.kt @@ -1,5 +1,6 @@ package com.foke.together.presenter.frame +import android.net.Uri import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -20,13 +21,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import coil.compose.AsyncImage import com.foke.together.presenter.R import com.foke.together.presenter.theme.FourCutTogetherTheme import com.foke.together.util.TimeUtil @Composable fun MakerFaireFrame( - cameraImageUrlList : List? = null, + cameraImageUrlList : List? = null, backgroundColor : Color = Color(0xFFF5B934), decorateImageUrl: String? = null, ) { @@ -52,6 +54,7 @@ fun MakerFaireFrame( .wrapContentSize() ) { items(4){ + itemIndex -> if(backgroundColor == Color.White) { //TODO: add camera image // change Box -> ImageView @@ -59,7 +62,11 @@ fun MakerFaireFrame( modifier = Modifier .aspectRatio(1.5f) .background(color = Color.Black) - ) + ){ + AsyncImage( + model = cameraImageUrlList?.get(itemIndex), + ) + } } else { //TODO: add camera image diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt index e50e177..a4f6eaa 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/CameraScreen.kt @@ -11,6 +11,9 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -37,6 +40,8 @@ fun CameraScreen( ) { val TAG = "CameraScreen" var mjpegView: MjpegView? = null + val context = LocalContext.current + var graphicsLayer = rememberGraphicsLayer() ConstraintLayout( modifier = Modifier.fillMaxSize() ) { @@ -64,7 +69,7 @@ fun CameraScreen( fontSize = 24.sp, ) - val mjpegPreview = AndroidView( + AndroidView( modifier = Modifier .constrainAs(preview) { top.linkTo(title.bottom) @@ -72,7 +77,16 @@ fun CameraScreen( end.linkTo(parent.end, margin = 24.dp) bottom.linkTo(imageCount.top) } - .aspectRatio(1.5f), + .aspectRatio(1.5f) + .drawWithContent { + // call record to capture the content in the graphics layer + graphicsLayer.record { + // draw the contents of the composable into the graphics layer + this@drawWithContent.drawContent() + } + // draw the graphics layer on the visible canvas + drawLayer(graphicsLayer) + }, factory = { context -> MjpegView(context).apply { mjpegView = this @@ -107,7 +121,7 @@ fun CameraScreen( } // test url // TODO : change url in viewmodel - setUrl("http://10.32.100.37:5000/preview") + setUrl("http://192.168.0.71:5000/preview") } }, ) @@ -126,7 +140,7 @@ fun CameraScreen( ) } LifecycleEventEffect(Lifecycle.Event.ON_START) { - viewModel.setCaptureTimer { navigateToShare() } + viewModel.setCaptureTimer(graphicsLayer) { navigateToShare() } AppLog.d(TAG, "ON_START") } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt index 3997aa7..90e2f6d 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/SelectFrameScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel +import com.foke.together.domain.interactor.entity.CutFrameSourceType import com.foke.together.presenter.R import com.foke.together.presenter.theme.FourCutTogetherTheme import com.foke.together.presenter.viewmodel.SelectFrameViewModel @@ -39,7 +40,7 @@ fun SelectFrameScreen( ) { FourCutTogetherTheme { val pagerState = rememberPagerState { - 3 // 총 페이지 수 설정 + CutFrameSourceType.entries.size } ConstraintLayout( modifier = Modifier.fillMaxSize() @@ -101,10 +102,11 @@ fun SelectFrameScreen( ) ) { page -> when(page){ - 0 -> Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_light), contentDescription = "fourcut_frame_medium_light") - 1 -> Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_dark), contentDescription = "fourcut_frame_medium_dark") - 2 -> Image(painter = painterResource(id = R.drawable.maker_faire_frame), contentDescription = "maker_faire_frame") + CutFrameSourceType.MAKER_FAIRE.ordinal -> Image(painter = painterResource(id = R.drawable.maker_faire_frame), contentDescription = "fourcut_frame_medium_light") + CutFrameSourceType.FOKE_LIGHT.ordinal -> Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_light), contentDescription = "fourcut_frame_medium_dark") + CutFrameSourceType.FOKE_DARK.ordinal -> Image(painter = painterResource(id = R.drawable.fourcut_frame_medium_dark), contentDescription = "maker_faire_frame") } + viewModel.setCutFrameSourceType(page) } IconButton( diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt index 73df5e4..97f0dec 100644 --- a/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt +++ b/presenter/src/main/java/com/foke/together/presenter/screen/ShareScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Print import androidx.compose.material.icons.filled.Share import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -19,6 +20,9 @@ import com.foke.together.presenter.frame.FourCutFrame import com.foke.together.presenter.theme.FourCutTogetherTheme import com.foke.together.presenter.theme.highContrastDarkColorScheme import androidx.hilt.navigation.compose.hiltViewModel +import com.foke.together.domain.interactor.entity.CutFrameSourceType +import com.foke.together.presenter.frame.MakerFaireFrame +import com.foke.together.presenter.theme.highContrastLightColorScheme import com.foke.together.presenter.viewmodel.ShareViewModel @Composable @@ -31,7 +35,8 @@ fun ShareScreen( ) { val (finalPic, printButton, shareButton, downloadButton, homeButton ) = createRefs() - val frameType = 0 +// val frameType = viewModel.cutFrameSourceType.collectAsState(initial = CutFrameSourceType.MAKER_FAIRE) + val frameType = CutFrameSourceType.MAKER_FAIRE val topGuideLine = createGuidelineFromTop(0.1f) val bottomGuideLine = createGuidelineFromBottom(0.1f) val startGuideLine = createGuidelineFromStart(0.2f) @@ -44,8 +49,6 @@ fun ShareScreen( ) // TODO: need check to change single ImageView - when(frameType){ - 0 -> { Card( modifier = Modifier .constrainAs(finalPic){ @@ -56,12 +59,22 @@ fun ShareScreen( height = Dimension.fillToConstraints } ){ - FourCutFrame( - designColorScheme = highContrastDarkColorScheme, - ) + when(frameType) { + CutFrameSourceType.MAKER_FAIRE -> { + MakerFaireFrame() + } + CutFrameSourceType.FOKE_LIGHT -> { + FourCutFrame( + designColorScheme = highContrastLightColorScheme, + ) + } + CutFrameSourceType.FOKE_DARK -> { + FourCutFrame( + designColorScheme = highContrastDarkColorScheme, + ) + } } } - } IconButton( onClick = { popBackStack() }, diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt index 8fbba74..ed8601a 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/CameraViewModel.kt @@ -1,20 +1,25 @@ package com.foke.together.presenter.viewmodel +import android.content.Context import android.os.CountDownTimer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.foke.together.util.AppLog +import com.foke.together.util.BitmapUtil import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CameraViewModel @Inject constructor( + @ApplicationContext private val context: Context ): ViewModel() { // TODO: progress State를 Flow로 구현하기 private val _progressState = mutableFloatStateOf(1f) @@ -22,12 +27,16 @@ class CameraViewModel @Inject constructor( private val _captureCount = mutableIntStateOf(1) val captureCount: Int by _captureCount private var captureTimer: CountDownTimer? = null - fun setCaptureTimer(nextNavigate: () -> Unit) { + fun setCaptureTimer( + graphicsLayer: GraphicsLayer, + nextNavigate: () -> Unit + ) { captureTimer = object : CountDownTimer(5000, 10) { override fun onTick(millisUntilFinished: Long) { _progressState.floatValue = 1f - (millisUntilFinished.toFloat() / 5000) } override fun onFinish() { + captureImage(graphicsLayer) _progressState.floatValue = 1f if(_captureCount.intValue < 4){ _captureCount.intValue += 1 @@ -49,4 +58,8 @@ class CameraViewModel @Inject constructor( if(captureTimer != null) captureTimer!!.cancel() } + fun captureImage(graphicsLayer: GraphicsLayer) = viewModelScope.launch { + BitmapUtil.saveBitmap(graphicsLayer, context,"together_${captureCount}") + } + } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt index cfc161a..72270e7 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/SelectFrameViewModel.kt @@ -1,11 +1,32 @@ package com.foke.together.presenter.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.foke.together.domain.interactor.GetCutFrameSourceTypeUseCase +import com.foke.together.domain.interactor.SetCutFrameSourceTypeUseCase +import com.foke.together.domain.interactor.entity.CutFrameSourceType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SelectFrameViewModel @Inject constructor( + getCutFrameSourceTypeUseCase: GetCutFrameSourceTypeUseCase, + private val setCutFrameSourceTypeUseCase: SetCutFrameSourceTypeUseCase ): ViewModel() { - // TODO: add viewmodel code here + val cutFrameSourceType = getCutFrameSourceTypeUseCase().shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + replay = 1 + ) + fun setCutFrameSourceType(index: Int){ + setCutFrameSourceType(CutFrameSourceType.entries[index]) + } + fun setCutFrameSourceType(type: CutFrameSourceType){ + viewModelScope.launch { + setCutFrameSourceTypeUseCase(type) + } + } } \ No newline at end of file diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt index 575622b..ceef1a7 100644 --- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt +++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt @@ -1,11 +1,48 @@ package com.foke.together.presenter.viewmodel +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.foke.together.domain.interactor.GetCutFrameSourceTypeUseCase +import com.foke.together.domain.interactor.SetCutFrameSourceTypeUseCase +import com.foke.together.domain.interactor.entity.CutFrameSourceType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn import javax.inject.Inject @HiltViewModel class ShareViewModel @Inject constructor( + getCutFrameSourceTypeUseCase: GetCutFrameSourceTypeUseCase, + private val setCutFrameSourceTypeUseCase: SetCutFrameSourceTypeUseCase ): ViewModel() { - // TODO: add viewmodel code here + val cutFrameSourceType = getCutFrameSourceTypeUseCase().shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + replay = 1 + ) +} + +@Composable +fun GenerateFrame( + frameType: CutFrameSourceType +){ + ConstraintLayout( + modifier = Modifier + .aspectRatio(ratio = 0.6666f) + ) { + LazyRow( + + ){ + + + } + } } \ No newline at end of file diff --git a/util/build.gradle.kts b/util/build.gradle.kts index 79887a7..360bfb8 100644 --- a/util/build.gradle.kts +++ b/util/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) + implementation(libs.androidx.ui.graphics.android) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/util/src/main/java/com/foke/together/util/BitmapUtil.kt b/util/src/main/java/com/foke/together/util/BitmapUtil.kt new file mode 100644 index 0000000..3f2415b --- /dev/null +++ b/util/src/main/java/com/foke/together/util/BitmapUtil.kt @@ -0,0 +1,82 @@ +package com.foke.together.util + +import android.content.Context +import android.content.Intent +import android.content.Intent.createChooser +import android.graphics.Bitmap +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Environment +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.core.content.ContextCompat.startActivity +import androidx.print.PrintHelper +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.File +import kotlin.coroutines.resume + +object BitmapUtil { + suspend fun saveBitmap( + graphicsLayer: GraphicsLayer, + context: Context, + fileName: String + ): Uri { + var uri : Uri = Uri.EMPTY + val bitmap = graphicsLayer.toImageBitmap() + bitmap.asAndroidBitmap().saveToDisk(context, fileName) + return uri + } + + private suspend fun Bitmap.saveToDisk(context: Context, fileName: String): Uri { + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + fileName + ".jpg" + ) + + file.writeBitmap(this, Bitmap.CompressFormat.JPEG, 100) + + return scanFilePath(context, file.path) ?: throw Exception("File could not be saved") + } + + private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { + outputStream().use { out -> + bitmap.compress(format, quality, out) + out.flush() + } + } + + /** + * We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible + * for other apps, as well as returning its [MediaStore] Uri + */ + private suspend fun scanFilePath(context: Context, filePath: String): Uri? { + return suspendCancellableCoroutine { continuation -> + MediaScannerConnection.scanFile( + context, + arrayOf(filePath), + arrayOf("image/jpg") + ) { _, scannedUri -> + if (scannedUri == null) { + continuation.cancel(Exception("File $filePath could not be scanned")) + } else { + continuation.resume(scannedUri) + } + } + } + } + + fun shareBitmap(context: Context, uri: Uri) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "image/jpg" + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(context, createChooser(intent, "Share your image"), null) + } + + fun printBitmap(context: Context, bitmap: Bitmap){ + // can use getActivity() + val photoPrinter = PrintHelper(context) + photoPrinter.printBitmap("Print", bitmap) + } +} \ No newline at end of file