diff options
Diffstat (limited to 'app/src/main')
7 files changed, 292 insertions, 30 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c3b055..74c226c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="foundation.e.privacycentralapp"> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <application android:name=".PrivacyCentralApplication" android:allowBackup="true" 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 + } +} diff --git a/app/src/main/res/drawable/ic_map_marker_blue.xml b/app/src/main/res/drawable/ic_map_marker_blue.xml new file mode 100644 index 0000000..619dc47 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_blue.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="512" + android:viewportHeight="512" + > + <path + android:fillColor="#FF5290F5" + android:pathData="M256,0C153.755,0 70.573,83.182 70.573,185.426c0,126.888 165.939,313.167 173.004,321.035c6.636,7.391 18.222,7.378 24.846,0c7.065,-7.868 173.004,-194.147 173.004,-321.035C441.425,83.182 358.244,0 256,0zM256,278.719c-51.442,0 -93.292,-41.851 -93.292,-93.293S204.559,92.134 256,92.134s93.291,41.851 93.291,93.293S307.441,278.719 256,278.719z" + /> +</vector> diff --git a/app/src/main/res/drawable/ic_map_marker_red.xml b/app/src/main/res/drawable/ic_map_marker_red.xml new file mode 100644 index 0000000..48fae25 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_red.xml @@ -0,0 +1,28 @@ +<!-- + ~ 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/>. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="512" + android:viewportHeight="512" + > + <path + android:fillColor="#FFEA4335" + android:pathData="M256,0C153.755,0 70.573,83.182 70.573,185.426c0,126.888 165.939,313.167 173.004,321.035c6.636,7.391 18.222,7.378 24.846,0c7.065,-7.868 173.004,-194.147 173.004,-321.035C441.425,83.182 358.244,0 256,0zM256,278.719c-51.442,0 -93.292,-41.851 -93.292,-93.293S204.559,92.134 256,92.134s93.291,41.851 93.291,93.293S307.441,278.719 256,278.719z" + /> +</vector> diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 1ebe9ef..38faf67 100644 --- a/app/src/main/res/layout/fragment_fake_location.xml +++ b/app/src/main/res/layout/fragment_fake_location.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:mapbox="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" @@ -98,13 +99,14 @@ /> </RadioGroup> - <ImageView - android:id="@+id/dummy_img_map" + + <foundation.e.privacycentralapp.features.location.FakeLocationMapView + android:id="@+id/mapView" android:layout_width="match_parent" - android:layout_height="254dp" + android:layout_height="240dp" + mapbox:mapbox_cameraZoom="15" android:layout_marginTop="32dp" android:layout_marginBottom="32dp" - android:src="@drawable/dummy_img_map_picker" /> <com.google.android.material.textfield.TextInputLayout @@ -143,9 +145,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + android:layout_marginTop="32dp" android:text="@string/add_location" app:backgroundTint="#007fff" - android:layout_marginTop="32dp" /> </LinearLayout> </androidx.core.widget.NestedScrollView> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff0cf0a..c0b8348 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,4 +35,6 @@ <string name="real_location_mode">Real location mode</string> <string name="random_location_mode">Random location mode</string> <string name="fake_location_mode">Fake location mode</string> + <string name="user_location_permission_explanation">This app needs location permissions in order to show its functionality.</string> + <string name="user_location_permission_not_granted">You didn\'t grant location permission</string> </resources>
\ No newline at end of file |