diff options
Diffstat (limited to 'app/src/main/java/foundation/e')
2 files changed, 241 insertions, 25 deletions
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 5b58293..24ea426 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 @@ -17,31 +17,72 @@ package foundation.e.privacycentralapp.features.location +import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.text.Editable import android.util.Log +import android.view.Gravity import android.view.View +import android.view.ViewGroup import android.widget.Button +import android.widget.FrameLayout import android.widget.ImageView import android.widget.RadioButton import android.widget.Toast import android.widget.Toolbar +import androidx.annotation.NonNull +import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout +import com.mapbox.android.core.permissions.PermissionsListener +import com.mapbox.android.core.permissions.PermissionsManager +import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.location.LocationComponent +import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions +import com.mapbox.mapboxsdk.location.modes.CameraMode +import com.mapbox.mapboxsdk.location.modes.RenderMode +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.style.layers.Property.NONE +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage +import com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility +import com.mapbox.mapboxsdk.style.layers.SymbolLayer +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.dummy.LocationMode import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch class FakeLocationFragment : Fragment(R.layout.fragment_fake_location), - MVIView<FakeLocationFeature.State, FakeLocationFeature.Action> { + MVIView<FakeLocationFeature.State, FakeLocationFeature.Action>, + PermissionsListener { + private lateinit var permissionsManager: PermissionsManager private val viewModel: FakeLocationViewModel by viewModels() + private lateinit var mapView: FakeLocationMapView + private lateinit var mapboxMap: MapboxMap + private var hoveringMarker: ImageView? = null + + private var mutableLatLongFlow = MutableStateFlow(LatLng()) + private var latLong = mutableLatLongFlow.asStateFlow() + + companion object { + private const val DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launchWhenStarted { @@ -62,6 +103,11 @@ class FakeLocationFragment : } } + override fun onAttach(context: Context) { + super.onAttach(context) + Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key)) + } + private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) .show() @@ -71,10 +117,38 @@ class FakeLocationFragment : super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById<Toolbar>(R.id.toolbar) setupToolbar(toolbar) + mapView = view.findViewById<FakeLocationMapView>(R.id.mapView) + .setup(savedInstanceState) { mapboxMap -> + this.mapboxMap = mapboxMap + 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 + } + mapView.addView(hoveringMarker) + initDroppedMarker(style) + mapboxMap.addOnCameraMoveListener { + mutableLatLongFlow.value = mapboxMap.cameraPosition.target + } + mapboxMap.addOnCameraIdleListener { Log.d("Mapview", "camera move ended") } + } + } bindClickListeners(view) } private fun bindClickListeners(fragmentView: View) { + val latEditText = + fragmentView.findViewById<TextInputLayout>(R.id.edittext_latitude).editText + val longEditText = + fragmentView.findViewById<TextInputLayout>(R.id.edittext_longitude).editText + fragmentView.let { it.findViewById<RadioButton>(R.id.radio_use_real_location) .setOnClickListener { radioButton -> @@ -90,15 +164,20 @@ class FakeLocationFragment : } it.findViewById<Button>(R.id.button_add_location) .setOnClickListener { - val latitude = - fragmentView.findViewById<TextInputLayout>(R.id.edittext_latitude).editText?.text.toString() - .toDouble() - val longitude = - fragmentView.findViewById<TextInputLayout>(R.id.edittext_longitude).editText?.text.toString() - .toDouble() + val latitude = latEditText?.text.toString().toDouble() + val longitude = longEditText?.text.toString().toDouble() saveSpecificLocation(latitude, longitude) } } + + lifecycleScope.launch { + latLong.collect { + latEditText?.text = + Editable.Factory.getInstance().newEditable(it.latitude.toString()) + longEditText?.text = + Editable.Factory.getInstance().newEditable(it.longitude.toString()) + } + } } private fun saveSpecificLocation(latitude: Double, longitude: Double) { @@ -144,28 +223,10 @@ class FakeLocationFragment : (state.location.mode == LocationMode.RANDOM_LOCATION) it.findViewById<RadioButton>(R.id.radio_use_real_location).isChecked = (state.location.mode == LocationMode.REAL_LOCATION) - it.findViewById<ImageView>(R.id.dummy_img_map).visibility = View.GONE - it.findViewById<TextInputLayout>(R.id.edittext_latitude).visibility = - View.GONE - it.findViewById<TextInputLayout>(R.id.edittext_longitude).visibility = - View.GONE - it.findViewById<Button>(R.id.button_add_location).visibility = View.GONE } LocationMode.CUSTOM_LOCATION -> view?.let { it.findViewById<RadioButton>(R.id.radio_use_specific_location).isChecked = true - it.findViewById<ImageView>(R.id.dummy_img_map).visibility = View.VISIBLE - it.findViewById<TextInputLayout>(R.id.edittext_latitude).apply { - visibility = View.VISIBLE - editText?.text = Editable.Factory.getInstance() - .newEditable(state.location.latitude.toString()) - } - it.findViewById<TextInputLayout>(R.id.edittext_longitude).apply { - visibility = View.VISIBLE - editText?.text = Editable.Factory.getInstance() - .newEditable(state.location.longitude.toString()) - } - it.findViewById<Button>(R.id.button_add_location).visibility = View.VISIBLE } } } @@ -175,4 +236,114 @@ class FakeLocationFragment : } override fun actions(): Flow<FakeLocationFeature.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 + ).build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.cameraMode = CameraMode.TRACKING + locationComponent.renderMode = RenderMode.NORMAL + } else { + permissionsManager = PermissionsManager(this) + permissionsManager.requestLocationPermissions(requireActivity()) + } + } + + private fun initDroppedMarker(loadedMapStyle: Style) { + // Add the marker image to map + loadedMapStyle.apply { + ResourcesCompat.getDrawable( + resources, + R.drawable.ic_map_marker_blue, + requireContext().theme + ) + ?.let { + addImage( + "dropped-icon-image", + it + ) + } + addSource(GeoJsonSource("dropped-marker-source-id")) + addLayer( + SymbolLayer( + DROPPED_MARKER_LAYER_ID, + "dropped-marker-source-id" + ).apply { + setProperties( + iconImage("dropped-icon-image"), + visibility(NONE), + iconAllowOverlap(true), + iconIgnorePlacement(true) + ) + } + ) + } + } + + override fun onStart() { + super.onStart() + mapView.onStart() + } + + override fun onResume() { + super.onResume() + mapView.onResume() + } + + override fun onPause() { + super.onPause() + mapView.onPause() + } + + override fun onStop() { + super.onStop() + mapView.onStop() + } + + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onDestroyView() { + super.onDestroyView() + mapView.onDestroy() + } + + override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) { + Toast.makeText( + requireContext(), + R.string.user_location_permission_explanation, + Toast.LENGTH_LONG + ).show() + } + + override fun onPermissionResult(granted: Boolean) { + if (granted) { + val style = mapboxMap.style + if (style != null) { + enableLocationPlugin(style) + } + } else { + Toast.makeText( + requireContext(), + R.string.user_location_permission_not_granted, + Toast.LENGTH_LONG + ).show() + } + } } + +fun FakeLocationMapView.setup(savedInstanceState: Bundle?, callback: OnMapReadyCallback) = + this.apply { + onCreate(savedInstanceState) + getMapAsync(callback) + } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt new file mode 100644 index 0000000..cd0030a --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.privacycentralapp.features.location + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import com.mapbox.mapboxsdk.maps.MapView + +class FakeLocationMapView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MapView(context, attrs, defStyleAttr) { + + /** + * Overrides onTouchEvent because this MapView is part of a scroll view + * and we want this map view to consume all touch events originating on this view. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true) + MotionEvent.ACTION_UP -> parent.requestDisallowInterceptTouchEvent(false) + } + super.onTouchEvent(event) + return true + } +} |