diff options
13 files changed, 373 insertions, 136 deletions
diff --git a/app/build.gradle b/app/build.gradle index 77e1113..5de3ca4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,14 +95,14 @@ dependencies { compileOnly files('libs/e-ui-sdk-1.0.1-q.jar') implementation files('libs/lineage-sdk.jar') implementation files('libs/trackerfilter.aar') - //implementation project(":privacymodulesapi") // include the google specific version of the modules, just for the google flavor - googleImplementation project(":privacymodulesgoogle") + //googleImplementation project(":privacymodulesgoogle") // include the e specific version of the modules, just for the e flavor - eImplementation project(":privacymodulese") + implementation 'foundation.e:privacymodule.api:0.4.1' + implementation 'foundation.e:privacymodule.e-29:0.3.2' implementation 'foundation.e:privacymodule.tor:0.1.1' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index d8c3047..5bd7c08 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -94,18 +94,20 @@ class DependencyContainer constructor(val app: Application) { private val fakeLocationStateUseCase by lazy { FakeLocationStateUseCase( - fakeLocationModule, permissionsModule, localStateRepository, CityDataSource, appDesc, GlobalScope) + fakeLocationModule, permissionsModule, localStateRepository, CityDataSource, appDesc, context, GlobalScope + ) } // ViewModelFactories val dashBoardViewModelFactory by lazy { - DashBoardViewModelFactory(getQuickPrivacyStateUseCase, ipScramblingStateUseCase, trackersStatisticsUseCase, trackersStateUseCase) + DashBoardViewModelFactory(getQuickPrivacyStateUseCase, ipScramblingStateUseCase, trackersStatisticsUseCase, trackersStateUseCase, fakeLocationStateUseCase) } val fakeLocationViewModelFactory by lazy { FakeLocationViewModelFactory( getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, - fakeLocationStateUseCase = fakeLocationStateUseCase) + fakeLocationStateUseCase = fakeLocationStateUseCase + ) } val blockerService = BlockerInterface.getInstance(context) diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt index 78cb4e4..145ff32 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt @@ -44,11 +44,14 @@ class LocalStateRepository(context: Context) { var fakeLocation: Pair<Float, Float>? get() = if (sharedPref.contains(KEY_FAKE_LATITUDE) && sharedPref.contains( - KEY_FAKE_LONGITUDE)) - Pair( - sharedPref.getFloat(KEY_FAKE_LATITUDE, 0f), - sharedPref.getFloat(KEY_FAKE_LONGITUDE, 0f)) - else null + KEY_FAKE_LONGITUDE + ) + ) + Pair( + sharedPref.getFloat(KEY_FAKE_LATITUDE, 0f), + sharedPref.getFloat(KEY_FAKE_LONGITUDE, 0f) + ) + else null set(value) { if (value == null) { sharedPref.edit() diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt index 02fdb0f..7f288e0 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt @@ -18,6 +18,11 @@ package foundation.e.privacycentralapp.domain.usecases import android.app.AppOpsManager +import android.content.Context +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.util.Log import foundation.e.privacycentralapp.data.repositories.LocalStateRepository import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.dummy.CityDataSource @@ -26,19 +31,23 @@ import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlin.random.Random class FakeLocationStateUseCase( private val fakeLocationModule: IFakeLocationModule, - private val permissionsModule: PermissionsPrivacyModule, private val localStateRepository: LocalStateRepository, private val citiesRepository: CityDataSource, private val appDesc: ApplicationDescription, + private val appContext: Context, private val coroutineScope: CoroutineScope ) { + private val _locationMode = MutableStateFlow(LocationMode.REAL_LOCATION) + val locationMode: StateFlow<LocationMode> = _locationMode init { coroutineScope.launch { @@ -48,24 +57,36 @@ class FakeLocationStateUseCase( } } - fun getLocationMode(): LocationMode = when(localStateRepository.fakeLocation) { - null -> LocationMode.REAL_LOCATION - in citiesRepository.citiesLocationsList -> LocationMode.RANDOM_LOCATION - else -> LocationMode.SPECIFIC_LOCATION + private val locationManager: LocationManager + get() = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + fun getLocationMode(): Triple<LocationMode, Float?, Float?> { + val fakeLocation = localStateRepository.fakeLocation + return if (fakeLocation != null && _locationMode.value == LocationMode.SPECIFIC_LOCATION) { + Triple( + LocationMode.SPECIFIC_LOCATION, + fakeLocation.first, + fakeLocation.second + ) + } else { + Triple(_locationMode.value, null, null) + } } private fun acquireLocationPermission() { - permissionsModule.toggleDangerousPermission(appDesc, - android.Manifest.permission.ACCESS_FINE_LOCATION, true) - - // permissionsModule.setAppOpMode( - // appDesc, AppOpsManager.OPSTR_COARSE_LOCATION, - // AppOpModes.ALLOWED - // ) - // permissionsModule.setAppOpMode( - // appDesc, AppOpsManager.OPSTR_FINE_LOCATION, - // AppOpModes.ALLOWED - // ) + permissionsModule.toggleDangerousPermission( + appDesc, + android.Manifest.permission.ACCESS_FINE_LOCATION, true + ) + + // permissionsModule.setAppOpMode( + // appDesc, AppOpsManager.OPSTR_COARSE_LOCATION, + // AppOpModes.ALLOWED + // ) + // permissionsModule.setAppOpMode( + // appDesc, AppOpsManager.OPSTR_FINE_LOCATION, + // AppOpModes.ALLOWED + // ) } private fun applySettings(isQuickPrivacyEnabled: Boolean, fakeLocation: Pair<Float, Float>?) { @@ -74,10 +95,12 @@ class FakeLocationStateUseCase( permissionsModule.setAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpModes.ALLOWED) } fakeLocationModule.startFakeLocation() - fakeLocationModule.setFakeLocation(fakeLocation.first.toDouble(), fakeLocation.second.toDouble()) + _locationMode.value = if (fakeLocation in citiesRepository.citiesLocationsList) LocationMode.RANDOM_LOCATION + else LocationMode.SPECIFIC_LOCATION } else { fakeLocationModule.stopFakeLocation() + _locationMode.value = LocationMode.REAL_LOCATION } } @@ -102,4 +125,70 @@ class FakeLocationStateUseCase( applySettings(true, null) } + private var listener: LocationListener? = null + + val currentLocation = MutableStateFlow<Location?>(null) + + private var localListener = object : LocationListener { + val providerName = LocationManager.NETWORK_PROVIDER + + override fun onLocationChanged(location: Location) { + Log.e("DebugLoc", "onLocationChanged $location") + currentLocation.value = location + } + + override fun onProviderEnabled(provider: String?) { + Log.e("DebugLoc", "ProvuderEnabled: $provider") + reset(provider) + } + + override fun onProviderDisabled(provider: String?) { + Log.e("DebugLoc", "ProvuderDisabled: $provider") + reset(provider) + } + + private fun reset(provider: String?) { + if (provider == providerName) { + stopListeningLocation() + currentLocation.value = null + startListeningLocation() + } + } + } + + fun startListeningLocation() { + requestLocationUpdates(localListener) + } + + fun stopListeningLocation() { + removeUpdates(localListener) + } + + fun requestLocationUpdates(listener: LocationListener) { + acquireLocationPermission() + try { + Log.e("DebugLoc", "requestLocationUpdates") + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, // TODO: tight this with fakelocation module. + 0L, + 0f, + listener + ) + // locationManager.requestLocationUpdates( + // LocationManager.NETWORK_PROVIDER, // TODO: tight this with fakelocation module. + // 0L, + // 0f, + // listener + // ) + + val location: Location? = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) + location?.let { listener.onLocationChanged(it) } + } catch (se: SecurityException) { + Log.e("DebugLoc", "Missing permission", se) + } + } + + fun removeUpdates(listener: LocationListener) { + locationManager.removeUpdates(listener) + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt index 39b6138..8d5c78b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt @@ -24,6 +24,7 @@ import foundation.e.flowmvi.SingleEventProducer import foundation.e.flowmvi.feature.BaseFeature import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode import foundation.e.privacycentralapp.domain.entities.LocationMode +import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase @@ -56,7 +57,7 @@ class DashboardFeature( val totalGraph: Int? = null, // val graphData val trackersCount: Int? = null, - val activeTrackersCount: Int? = null, + val dayTrackersCount: Int? = null, val dayStatistics: List<Int>? = null ) @@ -122,7 +123,8 @@ class DashboardFeature( getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, ipScramblingStateUseCase: IpScramblingStateUseCase, trackersStatisticsUseCase: TrackersStatisticsUseCase, - trackersStateUseCase: TrackersStateUseCase + trackersStateUseCase: TrackersStateUseCase, + fakeLocationStateUseCase: FakeLocationStateUseCase ): DashboardFeature = DashboardFeature( initialState = State(), @@ -140,6 +142,8 @@ class DashboardFeature( is Effect.TrackersBlockedUpdatedEffect -> state.copy( isAllTrackersBlocked = effect.areAllTrackersBlocked ) + is Effect.UpdateLocationModeEffect -> state.copy(locationMode = effect.mode) + /*is Effect.OpenDashboardEffect -> State.DashboardState( effect.trackersCount, effect.activeTrackersCount, @@ -214,6 +218,9 @@ class DashboardFeature( }, trackersStateUseCase.areAllTrackersBlocked.map { Effect.TrackersBlockedUpdatedEffect(it) + }, + fakeLocationStateUseCase.locationMode.map { + Effect.UpdateLocationModeEffect(it) } ) /* diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt index 248e358..75525f5 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt @@ -217,7 +217,7 @@ class DashboardFragment : binding.graph.invalidate() } - binding.graphLegend.text = getString(R.string.dashboard_graph_trackers_legend, state.trackersCount?.toString() ?: "No") + binding.graphLegend.text = getString(R.string.dashboard_graph_trackers_legend, state.dayTrackersCount?.toString() ?: "No") if (state.dayTrackersCount != null && state.trackersCount != null) { binding.amITracked.subTitle = getString(R.string.dashboard_am_i_tracked_subtitle, state.trackersCount, state.dayTrackersCount) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt index 4bf01d7..0dbcdda 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt @@ -20,6 +20,7 @@ package foundation.e.privacycentralapp.features.dashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import foundation.e.privacycentralapp.common.Factory +import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase @@ -32,7 +33,8 @@ class DashboardViewModel( private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, private val ipScramblingStateUseCase: IpScramblingStateUseCase, private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersStateUseCase: TrackersStateUseCase + private val trackersStateUseCase: TrackersStateUseCase, + private val fakeLocationStateUseCase: FakeLocationStateUseCase ) : ViewModel() { private val _actions = MutableSharedFlow<DashboardFeature.Action>() @@ -44,7 +46,8 @@ class DashboardViewModel( getPrivacyStateUseCase = getPrivacyStateUseCase, ipScramblingStateUseCase = ipScramblingStateUseCase, trackersStatisticsUseCase = trackersStatisticsUseCase, - trackersStateUseCase = trackersStateUseCase + trackersStateUseCase = trackersStateUseCase, + fakeLocationStateUseCase = fakeLocationStateUseCase ) } @@ -59,9 +62,10 @@ class DashBoardViewModelFactory( private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, private val ipScramblingStateUseCase: IpScramblingStateUseCase, private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersStateUseCase: TrackersStateUseCase + private val trackersStateUseCase: TrackersStateUseCase, + private val fakeLocationStateUseCase: FakeLocationStateUseCase ) : Factory<DashboardViewModel> { override fun create(): DashboardViewModel { - return DashboardViewModel(getPrivacyStateUseCase, ipScramblingStateUseCase, trackersStatisticsUseCase, trackersStateUseCase) + return DashboardViewModel(getPrivacyStateUseCase, ipScramblingStateUseCase, trackersStatisticsUseCase, trackersStateUseCase, fakeLocationStateUseCase) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt index 7c6a715..b1dc938 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt @@ -17,6 +17,7 @@ package foundation.e.privacycentralapp.features.location +import android.location.Location import android.util.Log import foundation.e.flowmvi.Actor import foundation.e.flowmvi.Reducer @@ -26,6 +27,7 @@ import foundation.e.privacycentralapp.domain.entities.LocationMode import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -48,45 +50,54 @@ class FakeLocationFeature( data class State( val isEnabled: Boolean, val mode: LocationMode, + val currentLocation: Location?, val specificLatitude: Float? = null, - val specificLongitude: Float? = null + val specificLongitude: Float? = null, + val forceRefresh: Boolean = false ) sealed class SingleEvent { object RandomLocationSelectedEvent : SingleEvent() object RealLocationSelectedEvent : SingleEvent() object SpecificLocationSavedEvent : SingleEvent() + data class LocationUpdatedEvent(val location: Location?) : SingleEvent() data class ErrorEvent(val error: String) : SingleEvent() } sealed class Action { object Init : Action() + object LeaveScreen : Action() // Action which is triggered everytime the location is updated. - //data class UpdateLocationAction(val latLng: LatLng) : Action() + // data class UpdateLocationAction(val latLng: LatLng) : Action() object UseRealLocationAction : Action() object UseRandomLocationAction : Action() data class SetSpecificLocationAction( val latitude: Float, - val longitude: Float) : Action() + val longitude: Float + ) : Action() } sealed class Effect { - data class QuickPrivacyUpdatedEffect(val isEnabled: Boolean): Effect() + data class QuickPrivacyUpdatedEffect(val isEnabled: Boolean) : Effect() data class LocationModeUpdatedEffect( val mode: LocationMode, val latitude: Float? = null, - val longitude: Float? = null) : Effect() + val longitude: Float? = null + ) : Effect() + data class LocationUpdatedEffect(val location: Location?) : Effect() data class ErrorEffect(val message: String) : Effect() object QuickPrivacyDisabledWarningEffect : Effect() + object NoEffect : Effect() } companion object { fun create( initialState: State = State( isEnabled = false, - mode = LocationMode.REAL_LOCATION + mode = LocationMode.REAL_LOCATION, + currentLocation = null ), getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, fakeLocationStateUseCase: FakeLocationStateUseCase, @@ -99,17 +110,46 @@ class FakeLocationFeature( is Effect.LocationModeUpdatedEffect -> state.copy( mode = effect.mode, specificLatitude = effect.latitude, - specificLongitude = effect.longitude) - - is Effect.ErrorEffect, - Effect.QuickPrivacyDisabledWarningEffect -> state + specificLongitude = effect.longitude + ) + // is Effect.LocationUpdatedEffect -> state.copy(currentLocation = effect.location) + Effect.QuickPrivacyDisabledWarningEffect -> state.copy(forceRefresh = !state.forceRefresh) + else -> state } }, actor = { state, action -> when (action) { is Action.Init -> merge( getQuickPrivacyStateUseCase.quickPrivacyEnabledFlow.map { Effect.QuickPrivacyUpdatedEffect(it) }, - flowOf(Effect.LocationModeUpdatedEffect(fakeLocationStateUseCase.getLocationMode()))) + flow { + fakeLocationStateUseCase.startListeningLocation() + val (mode, lat, lon) = fakeLocationStateUseCase.getLocationMode() + emit(Effect.LocationModeUpdatedEffect(mode = mode, latitude = lat, longitude = lon)) + }, + fakeLocationStateUseCase.currentLocation.map { Effect.LocationUpdatedEffect(it) } + + // callbackFlow { + // val listener = object : LocationListener { + // override fun onLocationChanged(location: Location) { + // Log.e("DebugLoc", "onLocationChanged $location") + // offer(Effect.LocationUpdatedEffect(location)) + // } + // + // override fun onProviderEnabled(provider: String?) { + // Log.e("DebugLoc", "ProvuderEnabled: $provider") + // } + // + // override fun onProviderDisabled(provider: String?) { + // Log.e("DebugLoc", "ProvuderDisabled: $provider") + // } + // } + // + // fakeLocationStateUseCase.requestLocationUpdates(listener) + // // TODO: when is awaitClose called ? + // awaitClose { fakeLocationStateUseCase.removeUpdates(listener) } + // } + + ) // is Action.UpdateLocationAction -> flowOf( // Effect.LocationUpdatedEffect( @@ -118,6 +158,10 @@ class FakeLocationFeature( // ) // ) + is Action.LeaveScreen -> { + fakeLocationStateUseCase.stopListeningLocation() + flowOf(Effect.NoEffect) + } is Action.SetSpecificLocationAction -> { if (state.isEnabled) { fakeLocationStateUseCase.setSpecificLocation( @@ -135,20 +179,22 @@ class FakeLocationFeature( } is Action.UseRandomLocationAction -> { if (state.isEnabled) { - fakeLocationStateUseCase.setRandomLocation() - flowOf(Effect.LocationModeUpdatedEffect(LocationMode.RANDOM_LOCATION)) - } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) + fakeLocationStateUseCase.setRandomLocation() + flowOf(Effect.LocationModeUpdatedEffect(LocationMode.RANDOM_LOCATION)) + } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) } is Action.UseRealLocationAction -> { if (state.isEnabled) { fakeLocationStateUseCase.stopFakeLocation() - flowOf(Effect.LocationModeUpdatedEffect(LocationMode.REAL_LOCATION)) + flowOf(Effect.LocationModeUpdatedEffect(LocationMode.REAL_LOCATION)) } else flowOf(Effect.QuickPrivacyDisabledWarningEffect) } } }, singleEventProducer = { _, _, effect -> when (effect) { + is Effect.LocationUpdatedEffect -> + SingleEvent.LocationUpdatedEvent(effect.location) Effect.QuickPrivacyDisabledWarningEffect -> SingleEvent.ErrorEvent("Enabled Quick Privacy to use functionalities") is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt index 0f69808..ed7b9be 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt @@ -19,14 +19,11 @@ package foundation.e.privacycentralapp.features.location import android.annotation.SuppressLint import android.content.Context +import android.location.Location import android.os.Bundle -import android.os.Looper import android.text.Editable import android.util.Log -import android.view.Gravity import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout import android.widget.ImageView import android.widget.Toast import androidx.annotation.NonNull @@ -39,7 +36,6 @@ import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.mapbox.android.core.location.LocationEngineCallback -import com.mapbox.android.core.location.LocationEngineRequest import com.mapbox.android.core.location.LocationEngineResult import com.mapbox.android.core.permissions.PermissionsListener import com.mapbox.android.core.permissions.PermissionsManager @@ -87,25 +83,26 @@ class FakeLocationFragment : private lateinit var binding: FragmentFakeLocationBinding - private lateinit var mapboxMap: MapboxMap - + private var mapboxMap: MapboxMap? = null + private var locationComponent: LocationComponent? = null private var hoveringMarker: ImageView? = null private var inputJob: Job? = null private var displayedLocation: Pair<Float, Float>? = null + // Callback which updates the map in realtime. private val locationChangeCallback: LocationEngineCallback<LocationEngineResult> = object : LocationEngineCallback<LocationEngineResult> { override fun onSuccess(result: LocationEngineResult?) { result?.lastLocation?.let { displayedLocation = it.latitude.toFloat() to it.longitude.toFloat() - mapboxMap.locationComponent.forceLocationUpdate( + mapboxMap?.locationComponent?.forceLocationUpdate( LocationUpdate.Builder().location(it).animationDuration(100) .build() ) if (!isCameraMoved) { - mapboxMap.animateCamera( + mapboxMap?.animateCamera( CameraUpdateFactory.newLatLng( LatLng( it.latitude, @@ -153,10 +150,11 @@ class FakeLocationFragment : is FakeLocationFeature.SingleEvent.ErrorEvent -> { displayToast(event.error) } + is FakeLocationFeature.SingleEvent.LocationUpdatedEvent -> + updateLocation(event.location) } } } - lifecycleScope.launchWhenStarted { viewModel.submitAction(Action.Init) } } override fun onAttach(context: Context) { @@ -180,48 +178,30 @@ class FakeLocationFragment : mapboxMap.getUiSettings().isRotateGesturesEnabled = false mapboxMap.setStyle(Style.MAPBOX_STREETS) { style -> enableLocationPlugin(style) - hoveringMarker = ImageView(requireContext()) - .apply { - setImageResource(R.drawable.mapbox_marker_icon_default) - val params = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER - ) - layoutParams = params + + mapboxMap.addOnCameraMoveListener { + if (binding.mapView.isEnabled) { + mapboxMap.cameraPosition.target.let { + viewModel.submitAction( + Action.SetSpecificLocationAction( + it.latitude.toFloat(), + it.longitude.toFloat() + ) + ) + } } - binding.mapView.addView(hoveringMarker) - hoveringMarker?.visibility = View.GONE // Keep hovering marker hidden by default - - // mapboxMap.addOnCameraMoveStartedListener { - // // Show marker when user starts to move across the map. - // hoveringMarker?.visibility = if (binding.mapView.isEnabled) { - // View.VISIBLE - // } else { - // View.GONE - // } - // isCameraMoved = true - // } - // - // mapboxMap.addOnCameraMoveListener { - // if (binding.mapView.isEnabled) { - // mapboxMap.cameraPosition.target.let { - // viewModel.submitAction( - // Action.SetSpecificLocationAction( - // it.latitude.toFloat(), - // it.longitude.toFloat() - // ) - // ) - // } - // } - - // } + } // Bind click listeners once map is ready. bindClickListeners() } } } - private fun getCoordinatesAfterTextChanged(inputLayout: TextInputLayout, editText: TextInputEditText, isLat: Boolean) = { editable: Editable? -> + private fun getCoordinatesAfterTextChanged( + inputLayout: TextInputLayout, + editText: TextInputEditText, + isLat: Boolean + ) = { editable: Editable? -> inputJob?.cancel() if (editable != null && editable.length > 0 && editText.isEnabled) { inputJob = lifecycleScope.launch { @@ -244,9 +224,16 @@ class FakeLocationFragment : val lat = binding.edittextLatitude.text.toString().toFloat() val lon = binding.edittextLongitude.text.toString().toFloat() if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) { - viewModel.submitAction(Action.SetSpecificLocationAction(lat, lon)) + Log.e("UpdateText", "") + mapboxMap?.moveCamera( + CameraUpdateFactory.newLatLng( + LatLng(lat.toDouble(), lon.toDouble()) + ) + ) + // viewModel.submitAction(Action.SetSpecificLocationAction(lat, lon)) } - } catch (e: NumberFormatException) {} + } catch (e: NumberFormatException) { + } } catch (e: NumberFormatException) { inputLayout.endIconMode = END_ICON_NONE inputLayout.error = getString(R.string.location_input_error) @@ -255,6 +242,7 @@ class FakeLocationFragment : } } + @SuppressLint("ClickableViewAccessibility") private fun bindClickListeners() { binding.radioUseRealLocation.setOnClickListener { viewModel.submitAction(Action.UseRealLocationAction) @@ -263,11 +251,23 @@ class FakeLocationFragment : viewModel.submitAction(Action.UseRandomLocationAction) } binding.radioUseSpecificLocation.setOnClickListener { - viewModel.submitAction( -Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocation?.second?: 0f) - ) + mapboxMap?.cameraPosition?.target?.let { + viewModel.submitAction( + Action.SetSpecificLocationAction(it.latitude.toFloat(), it.longitude.toFloat()) + ) + } } + // binding.mapView.addOnTouchListener { _, event -> + // //mapboxMap.addOnCameraMoveStartedListener { + // // Show marker when user starts to move across the map. + // if (event.action == ACTION_DOWN && binding.mapView.isEnabled) { + // hoveringMarker?.visibility = View.VISIBLE + // isCameraMoved = true + // } + // binding.mapView.onTouchEvent(event) + // } + binding.edittextLatitude.addTextChangedListener( afterTextChanged = getCoordinatesAfterTextChanged( binding.textlayoutLatitude, @@ -285,10 +285,8 @@ Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocatio ) } + @SuppressLint("MissingPermission") override fun render(state: FakeLocationFeature.State) { - hoveringMarker?.visibility = View.GONE - isCameraMoved = false - binding.radioUseRandomLocation.isChecked = (state.mode == LocationMode.RANDOM_LOCATION) binding.radioUseSpecificLocation.isChecked = (state.mode == LocationMode.SPECIFIC_LOCATION) @@ -296,6 +294,21 @@ Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocatio binding.mapView.isEnabled = (state.mode == LocationMode.SPECIFIC_LOCATION) + if (state.mode != LocationMode.SPECIFIC_LOCATION) { + isCameraMoved = false + binding.centeredMarker.isVisible = false + } else { + binding.mapLoader.isVisible = false + binding.mapOverlay.isVisible = false + binding.centeredMarker.isVisible = true + + mapboxMap?.moveCamera( + CameraUpdateFactory.newLatLng( + LatLng(state.specificLatitude?.toDouble() ?: 0.0, state.specificLongitude?.toDouble() ?: 0.0) + ) + ) + } + binding.textlayoutLatitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION) binding.textlayoutLongitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION) @@ -306,34 +319,74 @@ Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocatio override fun actions(): Flow<Action> = viewModel.actions @SuppressLint("MissingPermission") - private fun enableLocationPlugin(@NonNull loadedMapStyle: Style) { - // Check if permissions are enabled and if not request - if (PermissionsManager.areLocationPermissionsGranted(requireContext())) { - val locationComponent: LocationComponent = mapboxMap.locationComponent - locationComponent.activateLocationComponent( - LocationComponentActivationOptions.builder( - requireContext(), loadedMapStyle - ).useDefaultLocationEngine(true).build() - ) - locationComponent.isLocationComponentEnabled = true - locationComponent.cameraMode = CameraMode.TRACKING - locationComponent.renderMode = RenderMode.NORMAL - locationComponent.locationEngine?.let { - it.requestLocationUpdates( - LocationEngineRequest.Builder(DEFAULT_INTERVAL_IN_MILLISECONDS) - .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) - .setMaxWaitTime(DEFAULT_MAX_WAIT_TIME).build(), - locationChangeCallback, - Looper.getMainLooper() + private fun updateLocation(lastLocation: Location?) { + lastLocation?.let { location -> + locationComponent?.isLocationComponentEnabled = true + val locationUpdate = LocationUpdate.Builder() + .location(location) + .animationDuration(100) + .build() + locationComponent?.forceLocationUpdate(locationUpdate) + + // if (binding.mapView.isEnabled && !isCameraMoved) { + // binding.mapView.isEnabled = false + // mapboxMap?.moveCamera( + // CameraUpdateFactory.newLatLng( + // LatLng( + // location.latitude, + // location.longitude + // ) + // ) + // ) + // isCameraMoved = false + // binding.mapView.isEnabled = true + if (!binding.mapView.isEnabled) { + binding.mapLoader.isVisible = false + binding.mapOverlay.isVisible = false + mapboxMap?.animateCamera( + CameraUpdateFactory.newLatLng( + LatLng(location.latitude, location.longitude) + ) ) - it.getLastLocation(locationChangeCallback) } - } else { - permissionsManager = PermissionsManager(this) - permissionsManager.requestLocationPermissions(requireActivity()) + } ?: run { + locationComponent?.isLocationComponentEnabled = false + if (!binding.mapView.isEnabled) { + binding.mapLoader.isVisible = true + binding.mapOverlay.isVisible = true + } } } + @SuppressLint("MissingPermission") + private fun enableLocationPlugin(@NonNull loadedMapStyle: Style) { + // Check if permissions are enabled and if not request + // if (PermissionsManager.areLocationPermissionsGranted(requireContext())) { + locationComponent = mapboxMap?.locationComponent + locationComponent?.activateLocationComponent( + LocationComponentActivationOptions.builder( + requireContext(), loadedMapStyle + ).useDefaultLocationEngine(false).build() + ) + locationComponent?.isLocationComponentEnabled = true + locationComponent?.cameraMode = CameraMode.NONE + locationComponent?.renderMode = RenderMode.NORMAL + // //locationComponent.locationEngine?.let { + // it.requestLocationUpdates( + // LocationEngineRequest.Builder(DEFAULT_INTERVAL_IN_MILLISECONDS) + // .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + // .setMaxWaitTime(DEFAULT_MAX_WAIT_TIME).build(), + // locationChangeCallback, + // Looper.getMainLooper() + // ) + // it.getLastLocation(locationChangeCallback) + // } + // } else { + // permissionsManager = PermissionsManager(this) + // permissionsManager.requestLocationPermissions(requireActivity()) + // } + } + override fun onStart() { super.onStart() binding.mapView.onStart() @@ -341,11 +394,13 @@ Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocatio override fun onResume() { super.onResume() + viewModel.submitAction(Action.Init) binding.mapView.onResume() } override fun onPause() { super.onPause() + viewModel.submitAction(Action.LeaveScreen) binding.mapView.onPause() } @@ -374,7 +429,7 @@ Action.SetSpecificLocationAction(displayedLocation?.first?: 0f, displayedLocatio override fun onPermissionResult(granted: Boolean) { if (granted) { - val style = mapboxMap.style + val style = mapboxMap?.style if (style != null) { enableLocationPlugin(style) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt index 70ee0c1..4b91276 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt @@ -28,7 +28,8 @@ import kotlinx.coroutines.launch class FakeLocationViewModel( private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val fakeLocationStateUseCase: FakeLocationStateUseCase) : ViewModel() { + private val fakeLocationStateUseCase: FakeLocationStateUseCase +) : ViewModel() { private val _actions = MutableSharedFlow<FakeLocationFeature.Action>() val actions = _actions.asSharedFlow() @@ -37,7 +38,8 @@ class FakeLocationViewModel( FakeLocationFeature.create( getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, fakeLocationStateUseCase = fakeLocationStateUseCase, - coroutineScope = viewModelScope) + coroutineScope = viewModelScope + ) } fun submitAction(action: FakeLocationFeature.Action) { @@ -49,7 +51,8 @@ class FakeLocationViewModel( class FakeLocationViewModelFactory( private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val fakeLocationStateUseCase: FakeLocationStateUseCase) : Factory<FakeLocationViewModel> { + private val fakeLocationStateUseCase: FakeLocationStateUseCase +) : Factory<FakeLocationViewModel> { override fun create(): FakeLocationViewModel { return FakeLocationViewModel(getQuickPrivacyStateUseCase, fakeLocationStateUseCase) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt index ce7edd3..8d91425 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt @@ -29,7 +29,6 @@ import foundation.e.privacycentralapp.features.dashboard.DashboardFragment import foundation.e.trackerfilter.DNSBlockerService import foundation.e.trackerfilter.StatsIntentService - open class MainActivity : FragmentActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 558fde0..851a7c4 100644 --- a/app/src/main/res/layout/fragment_fake_location.xml +++ b/app/src/main/res/layout/fragment_fake_location.xml @@ -68,13 +68,41 @@ </RadioGroup> - <foundation.e.privacycentralapp.features.location.FakeLocationMapView - android:id="@+id/mapView" - android:layout_height="220dp" + <FrameLayout android:layout_marginTop="16dp" + android:layout_height="220dp" android:layout_width="match_parent" - mapbox:mapbox_cameraZoom="8" - /> + > + <foundation.e.privacycentralapp.features.location.FakeLocationMapView + android:id="@+id/mapView" + android:layout_height="match_parent" + android:layout_width="match_parent" + mapbox:mapbox_cameraZoom="8" + /> + <ImageView + android:id="@+id/centered_marker" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/mapbox_marker_icon_default" + android:layout_gravity="center" + android:visibility="gone" + /> + <View + android:id="@+id/map_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/grey_overlay" + android:visibility="visible" + /> + <ProgressBar + android:id="@+id/map_loader" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_margin="24dp" + android:layout_gravity="center" + android:visibility="visible" + /> + </FrameLayout> <com.google.android.material.textfield.TextInputLayout style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 85f0c70..f0414b6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -16,6 +16,7 @@ <color name="grey_text">#99000000</color> <color name="grey_text_2">#61000000</color> <color name="grey_divider">#14212121</color> + <color name="grey_overlay">#66FFFFFF</color> <color name="orange_off">#FC7222</color> <color name="green_on">#169659</color> |