Skip to content

Commit

Permalink
#17 in progress - additional unit test for SharedViewModel
Browse files Browse the repository at this point in the history
  • Loading branch information
mjureczko committed Oct 1, 2024
1 parent 20adfdf commit e49596b
Show file tree
Hide file tree
Showing 18 changed files with 189 additions and 58 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ dependencies {
testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.4.0'
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '4.4.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0'

androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:core:1.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ abstract class ReportAbstractTest {
val font: Typeface = ResourcesCompat.getFont(context, R.font.akaya_telivigala)!!
val treasuresProgress: TreasuresProgress = TreasuresProgress(ROUTE_NAME, TreasureDescription.nullObject())

fun createFacebookViewModel() = FacebookViewModel(storageHelper, context.resources, StandardTestDispatcher())
private val testDispatcher = StandardTestDispatcher()

fun createFacebookViewModel() = FacebookViewModel(storageHelper, context.resources, testDispatcher, testDispatcher)

fun saveEmptyProgress() = storageHelper.save(treasuresProgress)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import java.util.Date

@RunWith(AndroidJUnit4::class)
class ReportGeneratorTest {
private val testDispatcher = StandardTestDispatcher()

@Test
fun shouldCreateImage() {
//given
Expand All @@ -49,7 +51,7 @@ class ReportGeneratorTest {
StorageHelper(context).save(Route(treasuresProgress.routeName))

//when
val model = FacebookViewModel(StorageHelper(context), context.resources, StandardTestDispatcher())
val model = FacebookViewModel(StorageHelper(context), context.resources, testDispatcher, testDispatcher)
// //MapBox doesn't work in tests
var state = model.state.value
val mapIdx = state.elements.indices.find { state.elements[it].type == Type.MAP }!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import pl.marianjureczko.poszukiwacz.shared.StorageHelper
import java.io.File
import java.io.FileOutputStream

//TODO t: add unit test
class CustomInitializerForRoute(
private val storageHelper: StorageHelper,
private val assetManager: AssetManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ fun MainScreenBody(goToSearching: GoToSearching) {
)
Text(
text = state.messages[state.messageIndex].text,
style = MaterialTheme.typography.body1,
color = colorResource(R.color.colorPrimaryVariant),
textAlign = TextAlign.Justify,
modifier = Modifier.semantics { contentDescription = GUIDE_TEXT }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import pl.marianjureczko.poszukiwacz.shared.di.IoDispatcher
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
resources: Resources,
private val dispatcher: CoroutineDispatcher,
@IoDispatcher private val dispatcher: CoroutineDispatcher,
private val customInitializerForRoute: CustomInitializerForRoute
) : ViewModel() {
private var _state = mutableStateOf(MainState(resources))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import pl.marianjureczko.poszukiwacz.shared.PhotoHelper
import pl.marianjureczko.poszukiwacz.shared.di.IoDispatcher
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
private val photoHelper: PhotoHelper,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
private val TAG = javaClass.simpleName
private var _state: MutableState<CommemorativeState> = mutableStateOf(createState())
Expand All @@ -35,7 +38,7 @@ class CommemorativeViewModel @Inject constructor(
fun rotatePhoto() {
if(state.value.photoPath != null) {
viewModelScope.launch {
PhotoHelper.rotateGraphicClockwise(state.value.photoPath!!) {
PhotoHelper.rotateGraphicClockwise(ioDispatcher, state.value.photoPath!!) {
// refresh view
_state.value = _state.value.copy()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import pl.marianjureczko.poszukiwacz.shared.PhotoHelper
import pl.marianjureczko.poszukiwacz.shared.PhotoScalingHelper
import pl.marianjureczko.poszukiwacz.shared.RotatePhoto
import pl.marianjureczko.poszukiwacz.shared.StorageHelper
import pl.marianjureczko.poszukiwacz.shared.di.DefaultDispatcher
import pl.marianjureczko.poszukiwacz.shared.di.IoDispatcher
import javax.inject.Inject

//TODO: should be configurable for the sake of classic version
Expand All @@ -25,7 +27,8 @@ const val ROUTE_NAME = "custom"
class FacebookViewModel @Inject constructor(
private val storageHelper: StorageHelper,
private val resources: Resources,
private val dispatcher: CoroutineDispatcher
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {

private var _state: MutableState<FacebookState> = mutableStateOf(createState())
Expand All @@ -40,9 +43,9 @@ class FacebookViewModel @Inject constructor(
}

fun rotatePhoto(): RotatePhoto = { photoIdx: Int ->
viewModelScope.launch(dispatcher) {
viewModelScope.launch(defaultDispatcher) {
state.value.elements[photoIdx].photo?.let {
PhotoHelper.rotateGraphicClockwise(it) {
PhotoHelper.rotateGraphicClockwise(ioDispatcher, it) {
preparePhoto(photoIdx, it)
}
}
Expand Down Expand Up @@ -107,7 +110,7 @@ class FacebookViewModel @Inject constructor(
}

private fun preparePhoto(index: Int, photoFile: String) {
viewModelScope.launch(dispatcher) {
viewModelScope.launch(defaultDispatcher) {
val photo = BitmapFactory.decodeFile(photoFile)
val readyPhoto = PhotoScalingHelper.scalePhotoKeepRatio(photo, 250f, 300f)
val elements = state.value.elements.toMutableList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import com.google.android.gms.location.FusedLocationProviderClient
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) {
class LocationFetcher(
val context: Context,
val fusedLocationClient: FusedLocationProviderClient
) {

private val TAG = javaClass.simpleName
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var updateLocationCallback: (Location) -> Unit
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
Expand All @@ -29,9 +31,14 @@ class LocationFetcher(val context: Context) {
}
}

fun startFetching(interval: Long, viewModelScope: CoroutineScope, updateLocationCallback: (Location) -> Unit) {
fun startFetching(
interval: Long,
viewModelScope: CoroutineScope,
dispatcherMain: CoroutineDispatcher,
updateLocationCallback: (Location) -> Unit
) {
this.updateLocationCallback = updateLocationCallback
viewModelScope.launch {
viewModelScope.launch(dispatcherMain) {
requestLocation(interval)
}
}
Expand All @@ -41,15 +48,11 @@ class LocationFetcher(val context: Context) {
}

suspend fun requestLocation(interval: Long = 1_000) {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

val locationRequest = LocationRequest.Builder(interval).build()
return suspendCoroutine { _ ->
//The permission should be already granted, but Idea reports error when the check is missing
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch
import pl.marianjureczko.poszukiwacz.activity.result.n.NOTHING_FOUND_TREASURE_ID
import pl.marianjureczko.poszukiwacz.activity.result.n.ResultType
import pl.marianjureczko.poszukiwacz.activity.searching.ArcCalculator
import pl.marianjureczko.poszukiwacz.activity.searching.LocationCalculator
import pl.marianjureczko.poszukiwacz.model.HunterPath
import pl.marianjureczko.poszukiwacz.model.Route
import pl.marianjureczko.poszukiwacz.model.Treasure
Expand All @@ -26,6 +27,8 @@ import pl.marianjureczko.poszukiwacz.shared.GoToResults
import pl.marianjureczko.poszukiwacz.shared.PhotoHelper
import pl.marianjureczko.poszukiwacz.shared.ScanTreasureCallback
import pl.marianjureczko.poszukiwacz.shared.StorageHelper
import pl.marianjureczko.poszukiwacz.shared.di.IoDispatcher
import pl.marianjureczko.poszukiwacz.shared.di.MainDispatcher
import javax.inject.Inject

const val PARAMETER_ROUTE_NAME = "route_name"
Expand Down Expand Up @@ -58,20 +61,23 @@ interface CommemorativeSharedViewModel : DoCommemrative {
class SharedViewModel @Inject constructor(
private val storageHelper: StorageHelper,
private val locationFetcher: LocationFetcher,
private val locationCalculator: pl.marianjureczko.poszukiwacz.activity.searching.LocationCalculator,
private val locationCalculator: LocationCalculator,
private val photoHelper: PhotoHelper,
private val stateHandle: SavedStateHandle,
private val dispatcher: CoroutineDispatcher
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
) : SearchingViewModel, ResultSharedViewModel, SelectorSharedViewModel, CommemorativeSharedViewModel, ViewModel() {
private val TAG = javaClass.simpleName
private var _state: MutableState<SharedState> = mutableStateOf(createState())
private val arcCalculator = ArcCalculator()
//for test
var respawn: Boolean = true

init {
startFetchingLocation()
// after between screen navigation location updating may stop, hence needs to be retriggered periodically
viewModelScope.launch(dispatcher) {
while (true) {
viewModelScope.launch(ioDispatcher) {
while (respawn) {
locationFetcher.stopFetching()
startFetchingLocation()
delay(20_000)
Expand Down Expand Up @@ -116,7 +122,7 @@ class SharedViewModel @Inject constructor(
}

override fun resultPresented() {
viewModelScope.launch(dispatcher) {
viewModelScope.launch(ioDispatcher) {
delay(500)
if (state.value.treasuresProgress.resultRequiresPresentation == true) {
state.value.treasuresProgress.resultRequiresPresentation = false
Expand Down Expand Up @@ -164,7 +170,7 @@ class SharedViewModel @Inject constructor(
}

private fun startFetchingLocation() {
locationFetcher.startFetching(1_000, viewModelScope) { location ->
locationFetcher.startFetching(1_000, viewModelScope, mainDispatcher) { location ->
Log.i(TAG, "location updated")
val selectedTreasure = state.value.treasuresProgress.selectedTreasure
_state.value = _state.value.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ 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.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import pl.marianjureczko.poszukiwacz.activity.result.n.NOTHING_FOUND_TREASURE_ID
import pl.marianjureczko.poszukiwacz.shared.PhotoHelper
import pl.marianjureczko.poszukiwacz.shared.di.IoDispatcher
import javax.inject.Inject

const val PARAMETER_JUST_FOUND_TREASURE = "just_found_treasure_id"
Expand All @@ -22,7 +23,8 @@ const val PARAMETER_JUST_FOUND_TREASURE = "just_found_treasure_id"
class SelectorViewModel @Inject constructor(
private val stateHandle: SavedStateHandle,
private val photoHelper: PhotoHelper,
@ApplicationContext private val appContext: Context
@ApplicationContext private val appContext: Context,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
private val TAG = javaClass.simpleName
private var _state: MutableState<SelectorState> = mutableStateOf(createState(appContext))
Expand All @@ -32,7 +34,7 @@ class SelectorViewModel @Inject constructor(

fun delayedUpdateOfJustFound() {
if (state.value.justFoundTreasureId != NOTHING_FOUND_TREASURE_ID) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(ioDispatcher) {
delay(500)
state.value.justFoundTreasureId = NOTHING_FOUND_TREASURE_ID
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,22 @@ class HunterLocation : Serializable {
@field:Element
var latitude: Double
private set

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HunterLocation

if (longitude != other.longitude) return false
return latitude == other.latitude
}

override fun hashCode(): Int {
var result = longitude.hashCode()
result = 31 * result + latitude.hashCode()
return result
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class HunterPath() : Serializable {
@field:ElementList
private var locations = mutableListOf<HunterLocation>()

// Public getter for test purpose
val publicLocations: List<HunterLocation>
get() = locations

@field:Element(required = false)
private var start: Date? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.graphics.Matrix
import android.net.Uri
import android.util.Log
import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.withContext
Expand All @@ -25,8 +26,8 @@ class PhotoHelper(
/**
* @param photoFile absolute path to graphic file
*/
suspend fun rotateGraphicClockwise(photoFile: String, postExecute: Runnable) {
val result = withContext(Dispatchers.IO) {
suspend fun rotateGraphicClockwise(ioDispatcher: CoroutineDispatcher, photoFile: String, postExecute: Runnable) {
val result = withContext(ioDispatcher) {
val bitmap = BitmapFactory.decodeFile(photoFile)
val matrix = Matrix()
matrix.postRotate(90f)
Expand Down
Loading

0 comments on commit e49596b

Please sign in to comment.