diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/CommemorativeActivity.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/CommemorativeActivity.kt index 32f0820..5bf090e 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/CommemorativeActivity.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/CommemorativeActivity.kt @@ -10,7 +10,6 @@ import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import pl.marianjureczko.poszukiwacz.R -import pl.marianjureczko.poszukiwacz.activity.facebook.FacebookInputData import pl.marianjureczko.poszukiwacz.databinding.ActivityCommemorativeBinding import pl.marianjureczko.poszukiwacz.model.TreasuresProgress import pl.marianjureczko.poszukiwacz.shared.ActivityWithAdsAndBackButton @@ -53,7 +52,7 @@ class CommemorativeActivity : ActivityWithAdsAndBackButton() { rotatePhoto(input.photoAbsolutePath, model.commemorativePhotoUri()) } binding.doPhotoBtn.setOnClickListener { - doPhotoLauncher.launch(photoHelper.createCommemorativePhotoTempUri()) + doPhotoLauncher.launch(photoHelper.getCommemorativePhotoTempUri()) } setContentView(binding.root) diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeScreen.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeScreen.kt new file mode 100644 index 0000000..aed1a32 --- /dev/null +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeScreen.kt @@ -0,0 +1,163 @@ +package pl.marianjureczko.poszukiwacz.activity.commemorative.n + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import pl.marianjureczko.poszukiwacz.App +import pl.marianjureczko.poszukiwacz.R +import pl.marianjureczko.poszukiwacz.activity.searching.n.CommemorativeSharedState +import pl.marianjureczko.poszukiwacz.activity.searching.n.CommemorativeSharedViewModel +import pl.marianjureczko.poszukiwacz.activity.searching.n.SharedViewModel +import pl.marianjureczko.poszukiwacz.ui.components.AdvertBanner +import pl.marianjureczko.poszukiwacz.ui.components.TopBar +import pl.marianjureczko.poszukiwacz.ui.shareViewModelStoreOwner +import pl.marianjureczko.poszukiwacz.ui.theme.SecondaryBackground + +@SuppressLint("UnusedMaterialScaffoldPaddingParameter") +@Composable +fun CommemorativeScreen( + navController: NavController, + navBackStackEntry: NavBackStackEntry, + onClickOnGuide: () -> Unit, +) { + val scaffoldState: ScaffoldState = rememberScaffoldState() + Scaffold( + scaffoldState = scaffoldState, + topBar = { TopBar(navController, onClickOnGuide) }, + content = { + CommemorativeScreenBody( + navController, + shareViewModelStoreOwner(navBackStackEntry, navController), + scaffoldState + ) + } + ) +} + +@Composable +fun CommemorativeScreenBody( + navController: NavController, + viewModelStoreOwner: NavBackStackEntry, + scaffoldState: ScaffoldState +) { + val sharedViewModel: CommemorativeSharedViewModel = getViewModel(viewModelStoreOwner) + val sharedState = sharedViewModel.state.value as CommemorativeSharedState + val localViewModel: CommemorativeViewModel = hiltViewModel() + val localState: CommemorativeState = localViewModel.state.value + localViewModel.setPhotoPath( + sharedState.treasuresProgress.commemorativePhotosByTreasuresDescriptionIds[localState.treasureDesId]!! + ) + Column(Modifier.background(SecondaryBackground)) { + Spacer( + modifier = Modifier + .weight(0.01f) + .background(Color.Transparent) + ) + if (localState.photoPath != null) { + val photo: Bitmap = BitmapFactory.decodeFile(localState.photoPath) + val aspectRatio = photo.width.toFloat() / photo.height.toFloat() + Box( + modifier = Modifier + .fillMaxSize() + .weight(0.9f), + contentAlignment = Alignment.BottomEnd + ) { + Image( + painter = BitmapPainter(photo.asImageBitmap()), + contentDescription = "Photo from the hunt", + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + .align(Alignment.Center) + .aspectRatio(aspectRatio), + contentScale = ContentScale.FillBounds, + ) + DoPhotoButton(sharedViewModel, sharedState, localState) + Image( + painter = painterResource(id = R.drawable.rotate_arc), + contentDescription = "Rotate commemorative photo", + modifier = Modifier + .height(50.dp) + .offset(x = (-10).dp, y = (-70).dp) + .clickable { localViewModel.rotatePhoto() }, + contentScale = ContentScale.Inside + ) + } + } + Spacer( + modifier = Modifier + .weight(0.01f) + .background(Color.Transparent) + ) + AdvertBanner() + } +} + +@Composable +private fun DoPhotoButton( + sharedViewModel: CommemorativeSharedViewModel, + sharedState: CommemorativeSharedState, + localState: CommemorativeState +) { + val successMsg = stringResource(R.string.photo_replaced) + val failureMsg = stringResource(R.string.photo_not_replaced) + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + Toast.makeText(App.getAppContext(), successMsg, Toast.LENGTH_SHORT).show() + sharedViewModel.handleDoCommemorativePhotoResult( + sharedState.route.treasures.find { it.id == localState.treasureDesId }!! + )() + } else { + Toast.makeText(App.getAppContext(), failureMsg, Toast.LENGTH_SHORT).show() + } + } + ) + Image( + painter = painterResource(id = R.drawable.camera_do_photo), + contentDescription = "Do a new commemorative photo", + modifier = Modifier + .height(50.dp) + .offset(x = (-10).dp, y = (-10).dp) + .clickable { cameraLauncher.launch(localState.tempPhotoFileLocation) }, + contentScale = ContentScale.Inside + ) +} + +@Composable +private fun getViewModel(viewModelStoreOwner: NavBackStackEntry): CommemorativeSharedViewModel { + val viewModelDoNotInline: SharedViewModel = hiltViewModel(viewModelStoreOwner) + return viewModelDoNotInline +} \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeState.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeState.kt new file mode 100644 index 0000000..e73ed57 --- /dev/null +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeState.kt @@ -0,0 +1,9 @@ +package pl.marianjureczko.poszukiwacz.activity.commemorative.n + +import android.net.Uri + +data class CommemorativeState( + val treasureDesId: Int, + val tempPhotoFileLocation: Uri, + val photoPath: String? +) diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeViewModel.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeViewModel.kt new file mode 100644 index 0000000..4b0eaf5 --- /dev/null +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/commemorative/n/CommemorativeViewModel.kt @@ -0,0 +1,52 @@ +package pl.marianjureczko.poszukiwacz.activity.commemorative.n + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import pl.marianjureczko.poszukiwacz.shared.PhotoHelper +import javax.inject.Inject + +const val PARAMETER_TREASURE_DESCRIPTION_ID = "treasure_description_id" + +@HiltViewModel +class CommemorativeViewModel @Inject constructor( + private val stateHandle: SavedStateHandle, + private val photoHelper: PhotoHelper +) : ViewModel() { + private val TAG = javaClass.simpleName + private var _state: MutableState = mutableStateOf(createState()) + + val state: State + get() = _state + + fun setPhotoPath(photoPath: String) { + if (photoPath != state.value.photoPath) { + _state.value = _state.value.copy( + photoPath = photoPath + ) + } + } + + fun rotatePhoto() { + if(state.value.photoPath != null) { + viewModelScope.launch { + PhotoHelper.rotateGraphicClockwise(state.value.photoPath!!) { + // refresh view + _state.value = _state.value.copy() + } + } + } + } + + private fun createState(): CommemorativeState { + return CommemorativeState(stateHandle.get( + PARAMETER_TREASURE_DESCRIPTION_ID)!!, + photoHelper.getCommemorativePhotoTempUri(), + null) + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/MainActivity.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/MainActivity.kt index cb8f0b7..9ba0c56 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/MainActivity.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/MainActivity.kt @@ -17,6 +17,8 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import dagger.hilt.android.AndroidEntryPoint import pl.marianjureczko.poszukiwacz.R +import pl.marianjureczko.poszukiwacz.activity.commemorative.n.CommemorativeScreen +import pl.marianjureczko.poszukiwacz.activity.commemorative.n.PARAMETER_TREASURE_DESCRIPTION_ID import pl.marianjureczko.poszukiwacz.activity.map.n.MapScreen import pl.marianjureczko.poszukiwacz.activity.map.n.PARAMETER_ROUTE_NAME_2 import pl.marianjureczko.poszukiwacz.activity.photo.n.PARAMETER_TIP_PHOTO @@ -40,6 +42,8 @@ val RESULTS_PATH = "result" val RESULTS_ROUTE = "$RESULTS_PATH/{$PARAMETER_RESULT_TYPE}/{$PARAMETER_TREASURE_ID}" val SELECTOR_PATH = "selector" val SELECTOR_ROUTE = "$SELECTOR_PATH/{$PARAMETER_JUST_FOUND_TREASURE}" +val COMMEMORATIVE_PATH = "commemorative" +val COMMEMORATIVE_ROUTE = "$COMMEMORATIVE_PATH/{$PARAMETER_TREASURE_DESCRIPTION_ID}" /** * Routes creation and selection activity @@ -123,7 +127,7 @@ private fun ComposeRoot(settings: Settings, resources: Resources, onClickGuide: navArgument(PARAMETER_RESULT_TYPE) { type = NavType.EnumType(ResultType::class.java) }, navArgument(PARAMETER_TREASURE_ID) { type = NavType.IntType } ) - ) { navBackStackEntry -> ResultScreen(navController, navBackStackEntry, resources, onClickGuide) } + ) { navBackStackEntry -> ResultScreen(navController, navBackStackEntry, onClickGuide) } composable( route = "tipPhoto/{$PARAMETER_TIP_PHOTO}", arguments = listOf(navArgument(PARAMETER_TIP_PHOTO) { type = NavType.StringType }) @@ -144,7 +148,12 @@ private fun ComposeRoot(settings: Settings, resources: Resources, onClickGuide: navBackStackEntry, resources, onClickGuide, - goToResult = { treasureId -> navController.navigate("$RESULTS_PATH/${ResultType.TREASURE}/$treasureId") } + goToResult = { treasureId -> navController.navigate("$RESULTS_PATH/${ResultType.TREASURE}/$treasureId") }, + goToCommemorative = {treasureId -> navController.navigate("$COMMEMORATIVE_PATH/$treasureId")} ) } + composable( + route = COMMEMORATIVE_ROUTE, + arguments = listOf(navArgument(PARAMETER_TREASURE_DESCRIPTION_ID) { type = NavType.IntType }) + ) { navBackStackEntry -> CommemorativeScreen(navController, navBackStackEntry, onClickGuide) } } } \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/PermissionActivity.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/PermissionActivity.kt index 9f906ab..6848ec7 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/PermissionActivity.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/main/PermissionActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity import pl.marianjureczko.poszukiwacz.permissions.ActivityRequirements import pl.marianjureczko.poszukiwacz.permissions.PermissionListener import pl.marianjureczko.poszukiwacz.permissions.PermissionManager +import pl.marianjureczko.poszukiwacz.permissions.RequirementsForDoingPhoto import pl.marianjureczko.poszukiwacz.permissions.RequirementsForNavigation abstract class PermissionActivity : ComponentActivity() { @@ -42,6 +43,9 @@ abstract class PermissionActivity : ComponentActivity() { } permissionManager = PermissionManager(permissionListener) assurePermissionsAreGranted(RequirementsForNavigation, true) + //TODO: exitOnDenied==true and then exitOnDenied==false presumably leads to race condition + assurePermissionsAreGranted(RequirementsForDoingPhoto, false) +// assurePermissionsAreGranted(RequirementsForExternalStorage, false) } /** diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/ResultActivity.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/ResultActivity.kt index 7920108..e8a7b36 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/ResultActivity.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/ResultActivity.kt @@ -86,7 +86,7 @@ class ResultActivity : ActivityWithAdsAndBackButton() { } else { binding.doPhoto.setImageResource(R.drawable.camera_do_photo) binding.doPhoto.setOnClickListener { - doPhotoLauncher.launch(photoHelper.createCommemorativePhotoTempUri()) + doPhotoLauncher.launch(photoHelper.getCommemorativePhotoTempUri()) } } } diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/n/ResultScreen.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/n/ResultScreen.kt index dab6b60..a86b5b0 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/n/ResultScreen.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/result/n/ResultScreen.kt @@ -1,7 +1,6 @@ package pl.marianjureczko.poszukiwacz.activity.result.n import android.annotation.SuppressLint -import android.content.res.Resources import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -11,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp @@ -31,19 +31,18 @@ import pl.marianjureczko.poszukiwacz.ui.theme.SecondaryBackground fun ResultScreen( navController: NavController, navBackStackEntry: NavBackStackEntry, - resources: Resources, onClickOnGuide: () -> Unit ) { Scaffold( topBar = { TopBar(navController, onClickOnGuide) }, content = { - ResultScreenBody(resources, shareViewModelStoreOwner(navBackStackEntry, navController)) + ResultScreenBody(shareViewModelStoreOwner(navBackStackEntry, navController)) } ) } @Composable -fun ResultScreenBody(resources: Resources, viewModelStoreOwner: NavBackStackEntry) { +fun ResultScreenBody(viewModelStoreOwner: NavBackStackEntry) { val localViewModel: ResultViewModel = hiltViewModel() val localState = localViewModel.state.value val sharedViewModel: ResultSharedViewModel = getViewModel(viewModelStoreOwner) @@ -59,9 +58,9 @@ fun ResultScreenBody(resources: Resources, viewModelStoreOwner: NavBackStackEntr .background(Color.Transparent) ) val text = if (localState.resultType == ResultType.NOT_A_TREASURE) { - resources.getString(R.string.not_a_treasure_msg) + stringResource(R.string.not_a_treasure_msg) } else { - resources.getString(R.string.treasure_already_taken_msg) + stringResource(R.string.treasure_already_taken_msg) } Text( fontSize = 60.sp, diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/LocationFetcher.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/LocationFetcher.kt index 9d1f4b7..2acc9e6 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/LocationFetcher.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/LocationFetcher.kt @@ -10,13 +10,12 @@ import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.coroutines.suspendCoroutine -class LocationFetcher(val context: Context, val dispatcher: CoroutineDispatcher) { +class LocationFetcher(val context: Context) { private val TAG = javaClass.simpleName private lateinit var fusedLocationClient: FusedLocationProviderClient @@ -32,7 +31,7 @@ class LocationFetcher(val context: Context, val dispatcher: CoroutineDispatcher) fun startFetching(interval: Long, viewModelScope: CoroutineScope, updateLocationCallback: (Location) -> Unit) { this.updateLocationCallback = updateLocationCallback - viewModelScope.launch(dispatcher) { + viewModelScope.launch { requestLocation(interval) } } diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedState.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedState.kt index 76c9a30..ebdcca0 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedState.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedState.kt @@ -24,6 +24,11 @@ interface SearchingSharedState { fun treasureFoundAndResultAlreadyPresented(): Boolean } +interface CommemorativeSharedState { + val route: Route + var treasuresProgress: TreasuresProgress +} + data class SharedState( override val mediaPlayer: MediaPlayer, override var route: Route, @@ -36,7 +41,7 @@ data class SharedState( override val distancesInSteps: Map = route.treasures .associate { it.id to null } .toMap() -) : SelectorSharedState, SearchingSharedState { +) : SelectorSharedState, SearchingSharedState, CommemorativeSharedState { constructor(mediaPlayer: MediaPlayer, route: Route, treasuresProgress: TreasuresProgress) : this(mediaPlayer, route, route.treasures[0], treasuresProgress, null, null) diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedViewModel.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedViewModel.kt index eb35945..232e8a9 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedViewModel.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SharedViewModel.kt @@ -1,6 +1,5 @@ package pl.marianjureczko.poszukiwacz.activity.searching.n -import android.content.res.Resources import android.media.MediaPlayer import android.util.Log import androidx.compose.runtime.MutableState @@ -22,6 +21,7 @@ import pl.marianjureczko.poszukiwacz.model.Route import pl.marianjureczko.poszukiwacz.model.Treasure import pl.marianjureczko.poszukiwacz.model.TreasureDescription import pl.marianjureczko.poszukiwacz.model.TreasuresProgress +import pl.marianjureczko.poszukiwacz.shared.PhotoHelper import pl.marianjureczko.poszukiwacz.shared.StorageHelper import javax.inject.Inject @@ -41,6 +41,13 @@ interface SelectorSharedViewModel { fun updateJustFoundFromSelector() fun selectorPresented() fun updateCurrentTreasure(treasure: TreasureDescription) + fun handleDoCommemorativePhotoResult(treasure: TreasureDescription): () -> Unit +} + +interface CommemorativeSharedViewModel { + val state: State + + fun handleDoCommemorativePhotoResult(treasure: TreasureDescription): () -> Unit } @HiltViewModel @@ -48,10 +55,10 @@ class SharedViewModel @Inject constructor( private val storageHelper: StorageHelper, private val locationFetcher: LocationFetcher, private val locationCalculator: LocationCalculator, + private val photoHelper: PhotoHelper, private val stateHandle: SavedStateHandle, - private val dispatcher: CoroutineDispatcher, - private val resources: Resources -) : SearchingViewModel, ResultSharedViewModel, SelectorSharedViewModel, ViewModel() { + private val dispatcher: CoroutineDispatcher +) : SearchingViewModel, ResultSharedViewModel, SelectorSharedViewModel, CommemorativeSharedViewModel, ViewModel() { private val TAG = javaClass.simpleName private var _state: MutableState = mutableStateOf(createState()) private val arcCalculator = ArcCalculator() @@ -132,6 +139,15 @@ class SharedViewModel @Inject constructor( _state.value = _state.value.copy(currentTreasure = treasure) } + override fun handleDoCommemorativePhotoResult(treasure: TreasureDescription): () -> Unit { + return { + val target = _state.value.treasuresProgress.getCommemorativePhoto(treasure) + ?: storageHelper.newCommemorativePhotoFile() + photoHelper.moveCommemorativePhotoToPermanentLocation(target) + _state.value.treasuresProgress.addCommemorativePhoto(treasure, target) + } + } + override fun onCleared() { Log.i(TAG, "ViewModel cleared") locationFetcher.stopFetching() diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/TreasureProgressHolder.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/TreasureProgressHolder.kt index 34d58a3..7243da3 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/TreasureProgressHolder.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/TreasureProgressHolder.kt @@ -63,7 +63,7 @@ class TreasureProgressHolder( photoBtn.setOnClickListener { if (commemorativePhoto == null) { model.selectForCommemorativePhoto(treasure) - doPhotoLauncher.launch(photoHelper.createCommemorativePhotoTempUri()) + doPhotoLauncher.launch(photoHelper.getCommemorativePhotoTempUri()) } else { showCommemorativeLauncher.launch(CommemorativeInputData(commemorativePhoto, model.progress)) } diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorScreen.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorScreen.kt index fbb5d9f..8a11042 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorScreen.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorScreen.kt @@ -2,6 +2,9 @@ package pl.marianjureczko.poszukiwacz.activity.treasureselector.n import android.annotation.SuppressLint import android.content.res.Resources +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -28,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign @@ -35,6 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController +import pl.marianjureczko.poszukiwacz.App import pl.marianjureczko.poszukiwacz.R import pl.marianjureczko.poszukiwacz.activity.searching.n.SelectorSharedState import pl.marianjureczko.poszukiwacz.activity.searching.n.SelectorSharedViewModel @@ -52,7 +57,8 @@ fun SelectorScreen( navBackStackEntry: NavBackStackEntry, resources: Resources, onClickOnGuide: () -> Unit, - goToResult: (Int) -> Unit + goToResult: (Int) -> Unit, + goToCommemorative: (Int) -> Unit ) { val scaffoldState: ScaffoldState = rememberScaffoldState() Scaffold( @@ -64,7 +70,8 @@ fun SelectorScreen( shareViewModelStoreOwner(navBackStackEntry, navController), resources, scaffoldState, - goToResult + goToResult, + goToCommemorative ) } ) @@ -76,7 +83,8 @@ fun SelectorScreenBody( viewModelStoreOwner: NavBackStackEntry, resources: Resources, scaffoldState: ScaffoldState, - goToResult: (Int) -> Unit + goToResult: (Int) -> Unit, + goToCommemorative: (Int) -> Unit ) { val localViewModel: SelectorViewModel = hiltViewModel() val localState: SelectorState = localViewModel.state.value @@ -89,7 +97,16 @@ fun SelectorScreenBody( modifier = Modifier.weight(0.99f) ) { items(state.route.treasures) { treasure -> - TreasureItem(navController, resources, treasure, localState, state, goToResult, sharedViewModel) + TreasureItem( + navController, + resources, + treasure, + localState, + state, + goToResult, + sharedViewModel, + goToCommemorative + ) } } Spacer(modifier = Modifier.weight(0.01f)) @@ -107,7 +124,8 @@ fun TreasureItem( localState: SelectorState, state: SelectorSharedState, goToResult: (Int) -> Unit, - sharedViewModel: SelectorSharedViewModel + sharedViewModel: SelectorSharedViewModel, + goToCommemorative: (Int) -> Unit ) { Card( elevation = 4.dp, @@ -153,7 +171,13 @@ fun TreasureItem( .semantics { this.contentDescription = "Waiting for GPS" }) } ShowMovieButton(state, treasure, goToResult) - PhotoButton(state, treasure) + CommemorativePhotoButton( + state, + localState, + treasure, + goToCommemorative, + sharedViewModel.handleDoCommemorativePhotoResult(treasure) + ) } } } @@ -174,25 +198,48 @@ fun ShowMovieButton(state: SelectorSharedState, treasure: TreasureDescription, g } @Composable -private fun PhotoButton(sharedState: SelectorSharedState, treasure: TreasureDescription) { - if (sharedState.hasCommemorativePhoto(treasure.id)) { - Image( - painterResource(R.drawable.camera_show_photo), - modifier = Modifier - .padding(2.dp) - .height(35.dp), - contentDescription = "Show commemorative photo", - contentScale = ContentScale.Inside, - ) - } else { - Image( - painterResource(R.drawable.camera_do_photo), - modifier = Modifier - .padding(2.dp) - .height(35.dp), - contentDescription = "Do commemorative photo", - contentScale = ContentScale.Inside, - ) +private fun CommemorativePhotoButton( + sharedState: SelectorSharedState, + localState: SelectorState, + treasure: TreasureDescription, + goToCommemorative: (Int) -> Unit, + handleDoPhotoResult: () -> Unit +) { + if (localState.cameraPermissionGranted) { + if (sharedState.hasCommemorativePhoto(treasure.id)) { + Image( + painterResource(R.drawable.camera_show_photo), + modifier = Modifier + .padding(2.dp) + .height(35.dp) + .clickable { goToCommemorative(treasure.id) }, + contentDescription = "Show commemorative photo", + contentScale = ContentScale.Inside, + ) + } else { + val successMsg = stringResource(R.string.photo_replaced) + val failureMsg = stringResource(R.string.photo_not_replaced) + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + handleDoPhotoResult() + Toast.makeText(App.getAppContext(), successMsg, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(App.getAppContext(), failureMsg, Toast.LENGTH_SHORT).show() + } + } + ) + Image( + painterResource(R.drawable.camera_do_photo), + modifier = Modifier + .padding(2.dp) + .height(35.dp) + .clickable { cameraLauncher.launch(localState.tempPhotoFileLocation) }, + contentDescription = "Do commemorative photo", + contentScale = ContentScale.Inside, + ) + } } } diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorState.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorState.kt index 5fe5ed2..a4fce43 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorState.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorState.kt @@ -1,5 +1,9 @@ package pl.marianjureczko.poszukiwacz.activity.treasureselector.n +import android.net.Uri + data class SelectorState( - var justFoundTreasureId: Int + var justFoundTreasureId: Int, + var tempPhotoFileLocation: Uri, + var cameraPermissionGranted: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorViewModel.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorViewModel.kt index a665af3..4efc262 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorViewModel.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/activity/treasureselector/n/SelectorViewModel.kt @@ -1,6 +1,6 @@ package pl.marianjureczko.poszukiwacz.activity.treasureselector.n -import android.content.res.Resources +import android.content.Context import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf @@ -8,10 +8,14 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import pl.marianjureczko.poszukiwacz.activity.result.n.NOTHING_FOUND_TREASURE_ID +import pl.marianjureczko.poszukiwacz.permissions.PermissionManager +import pl.marianjureczko.poszukiwacz.permissions.PermissionsSpec +import pl.marianjureczko.poszukiwacz.shared.PhotoHelper import javax.inject.Inject const val PARAMETER_JUST_FOUND_TREASURE = "just_found_treasure_id" @@ -19,10 +23,11 @@ const val PARAMETER_JUST_FOUND_TREASURE = "just_found_treasure_id" @HiltViewModel class SelectorViewModel @Inject constructor( private val stateHandle: SavedStateHandle, - private val resources: Resources + private val photoHelper: PhotoHelper, + @ApplicationContext private val appContext: Context ) : ViewModel() { private val TAG = javaClass.simpleName - private var _state: MutableState = mutableStateOf(createState()) + private var _state: MutableState = mutableStateOf(createState(appContext)) val state: State get() = _state @@ -36,8 +41,12 @@ class SelectorViewModel @Inject constructor( } } - private fun createState(): SelectorState { + private fun createState(appContext: Context): SelectorState { val justFoundTreasureId = stateHandle.get(PARAMETER_JUST_FOUND_TREASURE)!! - return SelectorState(justFoundTreasureId) + return SelectorState( + justFoundTreasureId, + photoHelper.getCommemorativePhotoTempUri(), + PermissionManager.isPermissionGranted(appContext, PermissionsSpec.CAMERA) + ) } } \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/PhotoHelper.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/PhotoHelper.kt index 6f5d7df..f018bed 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/PhotoHelper.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/PhotoHelper.kt @@ -81,7 +81,7 @@ class PhotoHelper( } } - fun createCommemorativePhotoTempUri(): Uri = createPhotoUri(getCommemorativePhotoTempFile()) + fun getCommemorativePhotoTempUri(): Uri = createPhotoUri(getCommemorativePhotoTempFile()) /** * @return absolute path to the commemorative photo @@ -125,11 +125,12 @@ class PhotoHelper( } } - private fun createPhotoUri(photoFile: File): Uri { + fun createPhotoUri(photoFile: File): Uri { if (!photoFile.exists()) { photoFile.createNewFile() } - return FileProvider.getUriForFile(context, "pl.marianjureczko.poszukiwacz.fileprovider", photoFile) + //TODO: configurable classic vs kalinowice + return FileProvider.getUriForFile(context, "pl.marianjureczko.poszukiwacz.kalinowice.fileprovider", photoFile) } } diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/di/SingletonModule.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/di/SingletonModule.kt index 14ea8a1..f5057a7 100644 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/di/SingletonModule.kt +++ b/app/src/main/java/pl/marianjureczko/poszukiwacz/shared/di/SingletonModule.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import pl.marianjureczko.poszukiwacz.activity.searching.LocationCalculator import pl.marianjureczko.poszukiwacz.activity.searching.n.LocationFetcher +import pl.marianjureczko.poszukiwacz.shared.PhotoHelper import pl.marianjureczko.poszukiwacz.shared.Settings import pl.marianjureczko.poszukiwacz.shared.StorageHelper import javax.inject.Singleton @@ -22,6 +23,12 @@ object SingletonModule { @Provides fun providesDispatcher(): CoroutineDispatcher = Dispatchers.IO + @Singleton + @Provides + fun photoHelper(@ApplicationContext appContext: Context, storageHelper: StorageHelper): PhotoHelper { + return PhotoHelper(appContext, storageHelper) + } + @Singleton @Provides fun storageHelper(@ApplicationContext appContext: Context): StorageHelper { @@ -48,7 +55,7 @@ object SingletonModule { @Singleton @Provides - fun locationService(@ApplicationContext appContext: Context, dispatcher: CoroutineDispatcher): LocationFetcher { - return LocationFetcher(appContext, dispatcher) + fun locationService(@ApplicationContext appContext: Context): LocationFetcher { + return LocationFetcher(appContext) } } \ No newline at end of file diff --git a/app/src/main/java/pl/marianjureczko/poszukiwacz/ui/components/ComposableToast.kt b/app/src/main/java/pl/marianjureczko/poszukiwacz/ui/components/ComposableToast.kt deleted file mode 100644 index 019a8ad..0000000 --- a/app/src/main/java/pl/marianjureczko/poszukiwacz/ui/components/ComposableToast.kt +++ /dev/null @@ -1,29 +0,0 @@ -package pl.marianjureczko.poszukiwacz.ui.components - -import androidx.compose.material.Snackbar -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import pl.marianjureczko.poszukiwacz.App - -@Composable -fun ComposableToast(message: String, duration: Int = 10) { - val context = App.getAppContext() - var visible by remember { mutableStateOf(false) } - - if (visible) { - Snackbar( -// modifier = Modifier.padding(16.dp), - content = { Text(message) } -// duration = duration - ) - } - - LaunchedEffect(true) { - visible = true - } -} diff --git a/app/src/test/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SearchingViewModelFixture.kt b/app/src/test/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SearchingViewModelFixture.kt index fadb296..9685446 100644 --- a/app/src/test/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SearchingViewModelFixture.kt +++ b/app/src/test/java/pl/marianjureczko/poszukiwacz/activity/searching/n/SearchingViewModelFixture.kt @@ -1,6 +1,5 @@ package pl.marianjureczko.poszukiwacz.activity.searching.n -import android.content.res.Resources import androidx.lifecycle.SavedStateHandle import com.journeyapps.barcodescanner.ScanIntentResult import com.ocadotechnology.gembus.test.some @@ -10,6 +9,7 @@ import org.mockito.BDDMockito import org.mockito.Mockito.mock import pl.marianjureczko.poszukiwacz.activity.searching.LocationCalculator import pl.marianjureczko.poszukiwacz.model.Route +import pl.marianjureczko.poszukiwacz.shared.PhotoHelper import pl.marianjureczko.poszukiwacz.shared.StorageHelper data class SearchingViewModelFixture( @@ -19,7 +19,7 @@ data class SearchingViewModelFixture( val storage: StorageHelper = mock(StorageHelper::class.java), val locationFetcher: LocationFetcher = mock(LocationFetcher::class.java), val savedState: SavedStateHandle = mock(SavedStateHandle::class.java), - val resources: Resources = mock(Resources::class.java) + val photoHelper: PhotoHelper = mock(PhotoHelper::class.java) ) { lateinit var route: Route @@ -30,7 +30,7 @@ data class SearchingViewModelFixture( route.treasures.first().qrCode = firstTreasureQrCode BDDMockito.given(storage.loadRoute(routeName)).willReturn(route) BDDMockito.given(storage.loadProgress(routeName)).willReturn(null) - return SharedViewModel(storage, locationFetcher, LocationCalculator(), savedState, dispatcher, resources) + return SharedViewModel(storage, locationFetcher, LocationCalculator(), photoHelper, savedState, dispatcher) } fun givenScanIntentResultForFirstTreasure(): ScanIntentResult {