diff --git a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt index 1a06295efb5..51448f9f4b3 100644 --- a/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/mifos/core/data/di/DataModule.kt @@ -1,5 +1,6 @@ package com.mifos.core.data.di + import com.mifos.core.data.repository.CenterDetailsRepository import com.mifos.core.data.repository.CenterListRepository import com.mifos.core.data.repository.CheckerInboxRepository @@ -7,6 +8,7 @@ import com.mifos.core.data.repository.CheckerInboxTasksRepository import com.mifos.core.data.repository.GroupDetailsRepository import com.mifos.core.data.repository.GroupsListRepository import com.mifos.core.data.repository.NewIndividualCollectionSheetRepository +import com.mifos.core.data.repository.PathTrackingRepository import com.mifos.core.data.repository.ReportCategoryRepository import com.mifos.core.data.repository_imp.CenterDetailsRepositoryImp import com.mifos.core.data.repository_imp.CenterListRepositoryImp @@ -15,6 +17,7 @@ import com.mifos.core.data.repository_imp.CheckerInboxTasksRepositoryImp import com.mifos.core.data.repository_imp.GroupDetailsRepositoryImp import com.mifos.core.data.repository_imp.GroupsListRepositoryImpl import com.mifos.core.data.repository_imp.NewIndividualCollectionSheetRepositoryImp +import com.mifos.core.data.repository_imp.PathTrackingRepositoryImp import com.mifos.core.data.repository_imp.ReportCategoryRepositoryImp import dagger.Binds import dagger.Module @@ -50,4 +53,7 @@ abstract class DataModule { @Binds internal abstract fun bindReportCategoryRepository(impl: ReportCategoryRepositoryImp): ReportCategoryRepository + + @Binds + internal abstract fun bindPathTrackingRepository(impl: PathTrackingRepositoryImp): PathTrackingRepository } \ No newline at end of file diff --git a/core/data/src/main/java/com/mifos/core/data/repository/PathTrackingRepository.kt b/core/data/src/main/java/com/mifos/core/data/repository/PathTrackingRepository.kt new file mode 100644 index 00000000000..427104fe8d1 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/repository/PathTrackingRepository.kt @@ -0,0 +1,12 @@ +package com.mifos.core.data.repository + +import com.mifos.core.objects.user.UserLocation + +/** + * Created by Aditya Gupta on 06/08/23. + */ +interface PathTrackingRepository { + + suspend fun getUserPathTracking(userId: Int): List + +} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingRepositoryImp.kt b/core/data/src/main/java/com/mifos/core/data/repository_imp/PathTrackingRepositoryImp.kt similarity index 69% rename from mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingRepositoryImp.kt rename to core/data/src/main/java/com/mifos/core/data/repository_imp/PathTrackingRepositoryImp.kt index 9ddff2fc782..03aa731377c 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingRepositoryImp.kt +++ b/core/data/src/main/java/com/mifos/core/data/repository_imp/PathTrackingRepositoryImp.kt @@ -1,5 +1,6 @@ -package com.mifos.mifosxdroid.activity.pathtracking +package com.mifos.core.data.repository_imp +import com.mifos.core.data.repository.PathTrackingRepository import com.mifos.core.network.datamanager.DataManagerDataTable import com.mifos.core.objects.user.UserLocation import rx.Observable @@ -11,7 +12,7 @@ import javax.inject.Inject class PathTrackingRepositoryImp @Inject constructor(private val dataManagerDataTable: DataManagerDataTable) : PathTrackingRepository { - override fun getUserPathTracking(userId: Int): Observable> { + override suspend fun getUserPathTracking(userId: Int): List { return dataManagerDataTable.getUserPathTracking(userId) } diff --git a/core/database/src/main/java/com/mifos/core/objects/user/UserLocation.kt b/core/database/src/main/java/com/mifos/core/objects/user/UserLocation.kt index 6fd6ea8efb6..49ba7512fbe 100644 --- a/core/database/src/main/java/com/mifos/core/objects/user/UserLocation.kt +++ b/core/database/src/main/java/com/mifos/core/objects/user/UserLocation.kt @@ -1,67 +1,24 @@ package com.mifos.core.objects.user -import android.os.Parcel import android.os.Parcelable -import android.os.Parcelable.Creator -import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize /** * Created by Rajan Maurya on 24/01/17. */ -class UserLocation : Parcelable { - @SerializedName("user_id") - var userId: Int? = null +@Parcelize +data class UserLocation( + var user_id: Int? = null, - @SerializedName("latlng") - var latlng: String? = null + var latlng: String? = null, - @SerializedName("start_time") - var startTime: String? = null + var start_time: String? = null, - @SerializedName("stop_time") - var stopTime: String? = null + var stop_time: String? = null, - @SerializedName("date") - var date: String? = null - var dateFormat: String? = "dd MMMM yyyy HH:mm" - var locale: String? = "en" - - constructor() {} - - override fun describeContents(): Int { - return 0 - } + var date: String? = null, - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeValue(userId) - dest.writeString(latlng) - dest.writeString(startTime) - dest.writeString(stopTime) - dest.writeString(date) - dest.writeString(dateFormat) - dest.writeString(locale) - } + var dateFormat: String? = "dd MMMM yyyy HH:mm", - protected constructor(`in`: Parcel) { - userId = `in`.readValue(Int::class.java.classLoader) as Int? - latlng = `in`.readString() - startTime = `in`.readString() - stopTime = `in`.readString() - date = `in`.readString() - dateFormat = `in`.readString() - locale = `in`.readString() - } - - companion object { - @JvmField - val CREATOR: Creator = object : Creator { - override fun createFromParcel(source: Parcel): UserLocation? { - return UserLocation(source) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } -} \ No newline at end of file + var locale: String? = "en" +) : Parcelable \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt new file mode 100644 index 00000000000..bfa8c32724c --- /dev/null +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt @@ -0,0 +1,144 @@ +package com.mifos.core.designsystem.component + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver + +@Composable +fun PermissionBox( + requiredPermissions: List, + title: Int, + description: Int? = null, + confirmButtonText: Int, + dismissButtonText: Int, + onGranted: @Composable (() -> Unit)? = null, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + var permissionGranted by remember { + mutableStateOf( + requiredPermissions.all { + ContextCompat.checkSelfPermission( + context, + it + ) == PackageManager.PERMISSION_GRANTED + } + ) + } + + var shouldShowPermissionRationale = + requiredPermissions.all { + (context as? Activity)?.let { it1 -> + ActivityCompat.shouldShowRequestPermissionRationale( + it1, it + ) + } == true + } + + var shouldDirectUserToApplicationSettings by remember { + mutableStateOf(false) + } + + val decideCurrentPermissionStatus: (Boolean, Boolean) -> String = + { permissionGranted, shouldShowPermissionRationale -> + if (permissionGranted) "Granted" + else if (shouldShowPermissionRationale) "Rejected" + else "Denied" + } + + var currentPermissionStatus by remember { + mutableStateOf( + decideCurrentPermissionStatus( + permissionGranted, + shouldShowPermissionRationale + ) + ) + } + + val multiplePermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + onResult = { permissionResults -> + val isGranted = + requiredPermissions.all { permissionResults[it] ?: false } + + permissionGranted = isGranted + + if (!isGranted) { + shouldShowPermissionRationale = + requiredPermissions.all { + ActivityCompat.shouldShowRequestPermissionRationale( + context as Activity, + it + ) + } + } + shouldDirectUserToApplicationSettings = + !shouldShowPermissionRationale && !permissionGranted + currentPermissionStatus = decideCurrentPermissionStatus( + permissionGranted, + shouldShowPermissionRationale + ) + }) + + DisposableEffect(key1 = lifecycleOwner, effect = { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START && + !permissionGranted && + !shouldShowPermissionRationale + ) { + multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + }) + + if (shouldShowPermissionRationale) { + MifosDialogBox( + showDialogState = shouldShowPermissionRationale, + onDismiss = { shouldShowPermissionRationale = false }, + title = title, + message = description, + confirmButtonText = confirmButtonText, + onConfirm = { + shouldShowPermissionRationale = false + multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) + }, + dismissButtonText = dismissButtonText + ) + } + + if (shouldDirectUserToApplicationSettings) { + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ).also { + context.startActivity(it) + } + } + + if (permissionGranted) { + if (onGranted != null) { + onGranted() + } + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetUserPathTrackingUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetUserPathTrackingUseCase.kt new file mode 100644 index 00000000000..39286291ca9 --- /dev/null +++ b/core/domain/src/main/java/com/mifos/core/domain/use_cases/GetUserPathTrackingUseCase.kt @@ -0,0 +1,21 @@ +package com.mifos.core.domain.use_cases + +import com.mifos.core.common.utils.Resource +import com.mifos.core.data.repository.PathTrackingRepository +import com.mifos.core.objects.user.UserLocation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class GetUserPathTrackingUseCase @Inject constructor(private val repository: PathTrackingRepository) { + + suspend operator fun invoke(userId: Int): Flow>> = flow { + try { + emit(Resource.Loading()) + val response = repository.getUserPathTracking(userId) + emit(Resource.Success(response)) + } catch (exception: Exception) { + emit(Resource.Error(exception.message.toString())) + } + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerDataTable.kt b/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerDataTable.kt index 8f4760b033d..f67c684031f 100644 --- a/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerDataTable.kt +++ b/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerDataTable.kt @@ -79,7 +79,7 @@ class DataManagerDataTable @Inject constructor( * @param userId UserId Id * @return List */ - fun getUserPathTracking(userId: Int): Observable> { + suspend fun getUserPathTracking(userId: Int): List { return mBaseApiManager.dataTableApi.getUserPathTracking(userId) } } \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/services/DataTableService.kt b/core/network/src/main/java/com/mifos/core/network/services/DataTableService.kt index f921f274f4b..d578996fe3b 100644 --- a/core/network/src/main/java/com/mifos/core/network/services/DataTableService.kt +++ b/core/network/src/main/java/com/mifos/core/network/services/DataTableService.kt @@ -52,5 +52,5 @@ interface DataTableService { ): Observable @GET(APIEndPoint.DATATABLES + "/user_tracking/{userId}") - fun getUserPathTracking(@Path("userId") userId: Int): Observable> + suspend fun getUserPathTracking(@Path("userId") userId: Int): List } \ No newline at end of file diff --git a/feature/path-tracking/.gitignore b/feature/path-tracking/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/feature/path-tracking/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/path-tracking/build.gradle.kts b/feature/path-tracking/build.gradle.kts new file mode 100644 index 00000000000..fd89808bcc2 --- /dev/null +++ b/feature/path-tracking/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.mifos.android.feature) + alias(libs.plugins.mifos.android.library.compose) + alias(libs.plugins.mifos.android.library.jacoco) +} + +android { + namespace = "com.mifos.feature.path.tracking" +} + +dependencies { + implementation(projects.core.domain) + + implementation(libs.androidx.material) + implementation(libs.accompanist.permission) + + implementation(libs.coil.kt.compose) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.core.testing) + androidTestImplementation(projects.core.testing) + + implementation(libs.maps.compose) +} \ No newline at end of file diff --git a/feature/path-tracking/consumer-rules.pro b/feature/path-tracking/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/feature/path-tracking/proguard-rules.pro b/feature/path-tracking/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/feature/path-tracking/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/path-tracking/src/androidTest/java/com/mifos/feature/path_tracking/ExampleInstrumentedTest.kt b/feature/path-tracking/src/androidTest/java/com/mifos/feature/path_tracking/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..4bee60f120f --- /dev/null +++ b/feature/path-tracking/src/androidTest/java/com/mifos/feature/path_tracking/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.mifos.feature.path_tracking + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.mifos.feature.path_tracking.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/path-tracking/src/main/AndroidManifest.xml b/feature/path-tracking/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/feature/path-tracking/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingScreen.kt b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingScreen.kt new file mode 100644 index 00000000000..0b907fd3e5a --- /dev/null +++ b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingScreen.kt @@ -0,0 +1,321 @@ +@file:OptIn( + ExperimentalMaterialApi::class, + ExperimentalPermissionsApi::class +) + +package com.mifos.feature.path_tracking + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MyLocation +import androidx.compose.material.icons.rounded.Stop +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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 androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.registerReceiver +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapUiSettings +import com.google.maps.android.compose.rememberCameraPositionState +import com.mifos.core.common.utils.Constants +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.component.PermissionBox +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.Black +import com.mifos.core.designsystem.theme.White +import com.mifos.core.objects.user.UserLatLng +import com.mifos.core.objects.user.UserLocation +import com.mifos.feature.path.tracking.R + +@Composable +fun PathTrackingScreen( + userId: Int, + onBackPressed: () -> Unit, + onPathTrackingClick: (List) -> Unit +) { + + val context = LocalContext.current + val viewModel: PathTrackingViewModel = hiltViewModel() + val state by viewModel.pathTrackingUiState.collectAsStateWithLifecycle() + val refreshState by viewModel.isRefreshing.collectAsStateWithLifecycle() + val userStatus by viewModel.userStatus.collectAsStateWithLifecycle() + + DisposableEffect(Unit) { + + val notificationReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (Constants.STOP_TRACKING == action) { + viewModel.loadPathTracking(userId) + } + } + } + registerReceiver( + context, + notificationReceiver, + IntentFilter(Constants.STOP_TRACKING), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + + onDispose { + context.unregisterReceiver(notificationReceiver) + } + } + + LaunchedEffect(Unit) { + viewModel.loadPathTracking(userId) + } + + PathTrackingScreen( + state = state, + onBackPressed = onBackPressed, + onRetry = { + viewModel.loadPathTracking(userId) + }, + onPathTrackingClick = onPathTrackingClick, + onRefresh = { + viewModel.refreshCenterList(userId) + }, + refreshState = refreshState, + userStatus = userStatus, + updateUserStatus = { viewModel.updateUserStatus(it) } + ) +} + +@Composable +fun PathTrackingScreen( + state: PathTrackingUiState, + onBackPressed: () -> Unit, + onRetry: () -> Unit, + onPathTrackingClick: (List) -> Unit, + onRefresh: () -> Unit, + refreshState: Boolean, + userStatus: Boolean, + updateUserStatus: (Boolean) -> Unit +) { + + val snackbarHostState = remember { SnackbarHostState() } + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshState, + onRefresh = onRefresh + ) + var checkPermission by remember { mutableStateOf(false) } + + + if (checkPermission) { + PermissionBox( + requiredPermissions = listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ), + title = R.string.feature_path_tracking_permission_required, + description = R.string.feature_path_tracking_approve_permission_description_location, + confirmButtonText = R.string.feature_path_tracking_proceed, + dismissButtonText = R.string.feature_path_tracking_dismiss, + onGranted = { + updateUserStatus(true) + } + ) + } + + MifosScaffold( + icon = MifosIcons.arrowBack, + title = stringResource(id = R.string.feature_path_tracking_track_my_path), + onBackPressed = onBackPressed, + actions = { + IconButton( + onClick = { + if (userStatus) { + // TODO stop Path Service + updateUserStatus(false) + } else { + checkPermission = true + } + } + ) { + Icon( + imageVector = if (userStatus) Icons.Rounded.Stop else Icons.Rounded.MyLocation, + contentDescription = null, + ) + } + }, + snackbarHostState = snackbarHostState + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + when (state) { + is PathTrackingUiState.Error -> { + MifosSweetError(message = stringResource(id = state.message)) { + onRetry() + } + } + + is PathTrackingUiState.Loading -> MifosCircularProgress() + + is PathTrackingUiState.PathTracking -> { + PathTrackingContent( + pathTrackingList = state.userLocations, + onPathTrackingClick = onPathTrackingClick + ) + } + } + PullRefreshIndicator( + refreshing = refreshState, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } + } +} + + +@Composable +fun PathTrackingContent( + pathTrackingList: List, + onPathTrackingClick: (List) -> Unit +) { + LazyColumn { + items(pathTrackingList) { pathTracking -> + PathTrackingItem( + pathTracking = pathTracking, + onPathTrackingClick = onPathTrackingClick + ) + } + } +} + +@Composable +fun PathTrackingItem( + pathTracking: UserLocation, + onPathTrackingClick: (List) -> Unit +) { + val latLngList = getLatLngList(pathTracking.latlng) + val latLng = latLngList[0] + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(LatLng(latLng.lat, latLng.lng), 15f) + } + val uiSettings by remember { + mutableStateOf(MapUiSettings(zoomControlsEnabled = false)) + } + + OutlinedCard( + modifier = Modifier + .padding(8.dp), + onClick = { + onPathTrackingClick(latLngList) + }, + colors = CardDefaults.outlinedCardColors(White) + ) { + GoogleMap( + modifier = Modifier + .fillMaxWidth() + .height(150.dp), + cameraPositionState = cameraPositionState, + uiSettings = uiSettings + ) + Text( + modifier = Modifier.padding(8.dp), + text = "${pathTracking.date} from ${pathTracking.start_time} to ${pathTracking.stop_time}", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + color = Black + ) + ) + } +} + +private fun getLatLngList(latLngString: String?): List { + val gson = Gson() + return gson.fromJson( + latLngString, + object : TypeToken>() {}.type + ) +} + + +class PathTrackingUiStateProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + PathTrackingUiState.Loading, + PathTrackingUiState.Error(R.string.feature_path_tracking_no_path_tracking_found), + PathTrackingUiState.Error(R.string.feature_path_tracking_failed_to_load_path_tracking), + PathTrackingUiState.PathTracking(samplePathTrackingList) + ) +} + +@Preview(showBackground = true) +@Composable +private fun PathTrackingScreenPreview( + @PreviewParameter(PathTrackingUiStateProvider::class) state: PathTrackingUiState +) { + PathTrackingScreen( + state = state, + onBackPressed = {}, + onRetry = {}, + onPathTrackingClick = {}, + onRefresh = {}, + refreshState = false, + userStatus = false, + updateUserStatus = {} + ) +} + +val samplePathTrackingList = List(10) { + UserLocation( + user_id = it, + latlng = "123,456", + date = "date $it", + start_time = "start time $it", + stop_time = "stop time $it", + ) +} \ No newline at end of file diff --git a/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingUiState.kt b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingUiState.kt new file mode 100644 index 00000000000..68256cb2311 --- /dev/null +++ b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingUiState.kt @@ -0,0 +1,17 @@ +package com.mifos.feature.path_tracking + +import com.mifos.core.objects.user.UserLocation + +/** + * Created by Aditya Gupta on 06/08/23. + */ + +sealed class PathTrackingUiState { + + data object Loading : PathTrackingUiState() + + data class Error(val message: Int) : PathTrackingUiState() + + data class PathTracking(val userLocations: List) : PathTrackingUiState() + +} \ No newline at end of file diff --git a/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingViewModel.kt b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingViewModel.kt new file mode 100644 index 00000000000..b0df0356224 --- /dev/null +++ b/feature/path-tracking/src/main/java/com/mifos/feature/path_tracking/PathTrackingViewModel.kt @@ -0,0 +1,64 @@ +package com.mifos.feature.path_tracking + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.Resource +import com.mifos.core.datastore.PrefManager +import com.mifos.core.domain.use_cases.GetUserPathTrackingUseCase +import com.mifos.feature.path.tracking.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PathTrackingViewModel @Inject constructor( + private val getUserPathTrackingUseCase: GetUserPathTrackingUseCase, + private val prefManager: PrefManager +) : ViewModel() { + + private val _pathTrackingUiState = + MutableStateFlow(PathTrackingUiState.Loading) + val pathTrackingUiState = _pathTrackingUiState.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + private val _userStatus = MutableStateFlow(prefManager.userStatus) + val userStatus = _userStatus.asStateFlow() + + fun refreshCenterList(userId: Int) { + _isRefreshing.value = true + loadPathTracking(userId) + _isRefreshing.value = false + } + + + fun loadPathTracking(userId: Int) = viewModelScope.launch(Dispatchers.IO) { + getUserPathTrackingUseCase(userId).collect { result -> + when (result) { + is Resource.Error -> _pathTrackingUiState.value = + PathTrackingUiState.Error(R.string.feature_path_tracking_failed_to_load_path_tracking) + + is Resource.Loading -> _pathTrackingUiState.value = PathTrackingUiState.Loading + + is Resource.Success -> + result.data?.let { pathTracking -> + _pathTrackingUiState.value = + if (pathTracking.isEmpty()) PathTrackingUiState.Error(R.string.feature_path_tracking_no_path_tracking_found) else PathTrackingUiState.PathTracking( + pathTracking + ) + } + } + + } + } + + fun updateUserStatus(status: Boolean) = viewModelScope.launch(Dispatchers.IO) { + prefManager.userStatus = status + _userStatus.value = status + } + +} \ No newline at end of file diff --git a/feature/path-tracking/src/main/res/values/strings.xml b/feature/path-tracking/src/main/res/values/strings.xml new file mode 100644 index 00000000000..b2e2ddaeeac --- /dev/null +++ b/feature/path-tracking/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + + Track my path + + Failed to load path tracking + No path tracking found + + Permissions Required + You need to approve these permissions in order to access Location. + Proceed + Dismiss + \ No newline at end of file diff --git a/feature/path-tracking/src/test/java/com/mifos/feature/path_tracking/ExampleUnitTest.kt b/feature/path-tracking/src/test/java/com/mifos/feature/path_tracking/ExampleUnitTest.kt new file mode 100644 index 00000000000..a560fdfad3c --- /dev/null +++ b/feature/path-tracking/src/test/java/com/mifos/feature/path_tracking/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.mifos.feature.path_tracking + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4b71f52a3e..441fd1ea27e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,6 +76,7 @@ lifecycleExtensions = "2.2.0" lifecycleReactivestreamsKtx = "2.7.0" lint = "31.3.0" leakcanaryVersion = '2.10' +mapsCompose = "4.3.3" material = "1.11.0" materialIconsExtended = "1.6.2" materialshowcaseview = "1.3.7" @@ -151,6 +152,7 @@ android-maps-utils = { module = "com.google.maps.android:android-maps-utils", ve android-iconify-material = { module = "com.joanzapata.iconify:android-iconify-material", version.ref = "androidIconifyMaterial" } #RxAndroid +maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" } rxandroid = { module = "io.reactivex:rxandroid", version.ref = "rxandroidVersion" } rxjava = { module = "io.reactivex:rxjava", version.ref = "rxjava" } diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 4bccc97d60d..c5e0b63508e 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -136,6 +136,7 @@ dependencies { implementation(projects.feature.center) implementation(projects.feature.about) implementation(projects.feature.report) + implementation(projects.feature.pathTracking) implementation(projects.core.common) implementation(projects.core.ui) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingActivity.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingActivity.kt index 976b6dc4e10..da1bf7fe2ef 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingActivity.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/activity/pathtracking/PathTrackingActivity.kt @@ -4,30 +4,14 @@ */ package com.mifos.mifosxdroid.activity.pathtracking -import android.Manifest -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.Button -import android.widget.Toast -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler -import com.mifos.core.objects.user.UserLocation +import androidx.activity.compose.setContent +import com.mifos.core.objects.user.UserLatLng +import com.mifos.feature.path_tracking.PathTrackingScreen import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.adapters.PathTrackingAdapter import com.mifos.mifosxdroid.core.MifosBaseActivity -import com.mifos.mifosxdroid.databinding.ActivityPathTrackerBinding -import com.mifos.utils.CheckSelfPermissionAndRequest -import com.mifos.utils.Constants import com.mifos.utils.PrefManager import dagger.hilt.android.AndroidEntryPoint @@ -35,247 +19,36 @@ import dagger.hilt.android.AndroidEntryPoint * @author fomenkoo */ @AndroidEntryPoint -class PathTrackingActivity : MifosBaseActivity(), OnRefreshListener { +class PathTrackingActivity : MifosBaseActivity() { - private lateinit var binding: ActivityPathTrackerBinding - private lateinit var viewModel: PathTrackingViewModel - - private var pathTrackingAdapter: PathTrackingAdapter? = null - private var intentLocationService: Intent? = null - private var notificationReceiver: BroadcastReceiver? = null - private var userLocations: List? = null - private var sweetUIErrorHandler: SweetUIErrorHandler? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityPathTrackerBinding.inflate(layoutInflater) - setContentView(binding.root) - viewModel = ViewModelProvider(this)[PathTrackingViewModel::class.java] - showBackButton() - intentLocationService = Intent(this, PathTrackingService::class.java) - createNotificationReceiver() - showUserInterface() - viewModel.loadPathTracking(PrefManager.getUserId()) - binding.layoutError.findViewById