CultureQuestORG/SDP2023

View on GitHub
app/src/main/java/ch/epfl/culturequest/ui/map/MapsFragment.java

Summary

Maintainability
C
7 hrs
Test Coverage
F
21%
package ch.epfl.culturequest.ui.map;

import android.Manifest;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import ch.epfl.culturequest.R;
import ch.epfl.culturequest.authentication.Authenticator;
import ch.epfl.culturequest.backend.map_collection.BasicOTMProvider;
import ch.epfl.culturequest.backend.map_collection.OTMLocation;
import ch.epfl.culturequest.backend.map_collection.OTMProvider;
import ch.epfl.culturequest.backend.map_collection.RetryingOTMProvider;
import ch.epfl.culturequest.database.Database;
import ch.epfl.culturequest.databinding.FragmentMapsBinding;
import ch.epfl.culturequest.social.Profile;
import ch.epfl.culturequest.utils.AndroidUtils;
import ch.epfl.culturequest.utils.PermissionRequest;

public class MapsFragment extends Fragment {

    private final static float DEFAULT_ZOOM = 15f;
    private final static float LONGITUDE_DIFF = 0.015449859201908112f;
    private final static float LATITUDE_DIFF = 0.023033438247566096f;
    private FusedLocationProviderClient fusedLocationClient;
    private LocationRequest locationRequest;

    private boolean firstLocationUpdate = true;

    private final static MapUnavailableFragment unavailableFragment = new MapUnavailableFragment();

    private FragmentMapsBinding binding;
    private ImageView centerButton;
    private MapsViewModel viewModel;

    private OTMProvider otmProvider;

    private boolean isWifiAvailable = true;
    private GoogleMap mMap;

    private Location lastKnownLocation;

    private ActivityResultLauncher<String> launcher =
            registerForActivityResult(new ActivityResultContracts.RequestPermission(),
                    this::onRequestPermissionsResult);
    private final PermissionRequest permissionRequest = new PermissionRequest(Manifest.permission.ACCESS_FINE_LOCATION);

    private Bitmap profilePicture;

    private Marker frame;
    private Marker profileMarker;
    private final OnMapReadyCallback callback = new OnMapReadyCallback() {

        /**
         * Manipulates the map once available.
         * This callback is triggered when the map is ready to be used.
         * This is where we can add markers or lines, add listeners or move the camera.
         * In this case, we just add a marker near Sydney, Australia.
         * If Google Play services is not installed on the device, the user will be prompted to
         * install it inside the SupportMapFragment. This method will only be triggered once the
         * user has installed Google Play services and returned to the app.
         */
        @Override
        public void onMapReady(GoogleMap googleMap) {
            if (!isWifiAvailable) return;
            mMap = googleMap;
            getProfilePicture();
            mMap.setMapStyle(MapStyleOptions.loadRawResourceStyle(getContext(), R.raw.maps_style_json_alternative));
            getLocationPermission();
            mMap.moveCamera(CameraUpdateFactory
                    .newLatLngZoom(viewModel.getCurrentLocation().getValue(), DEFAULT_ZOOM)); // Set to Default location anyway
            mMap.setOnInfoWindowClickListener(Marker::hideInfoWindow);
        }
    };

    private void checkInternet() {
        if (!AndroidUtils.hasConnection(this.getContext())) {
            isWifiAvailable = false;
            centerButton.setVisibility(View.GONE);
            this.getParentFragmentManager().beginTransaction().hide(this).show(unavailableFragment).setReorderingAllowed(true).commit();
            AndroidUtils.showNoConnectionAlert(getContext(), "It seems that you are not connected to the internet. You can't use the map without internet connection.");
        } else {
            isWifiAvailable = true;
            centerButton.setVisibility(View.VISIBLE);
            this.getParentFragmentManager().beginTransaction().hide(unavailableFragment).show(this).setReorderingAllowed(true).commit();
        }
    }

    private void drawPositionMarker(LatLng latestLocation) {
        frame = mMap.addMarker(new MarkerOptions().zIndex(10000f).position(latestLocation).icon(BitmapDescriptorFactory.fromBitmap(Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.map_icon_frame), 75, 75, false))));
        if (profilePicture != null) {
            profileMarker = mMap.addMarker(new MarkerOptions().zIndex(10001f).icon(BitmapDescriptorFactory.fromBitmap(Bitmap.createScaledBitmap(profilePicture, 70, 70, false))).position(latestLocation));
        }
    }

    // Method to change the profile picture from square to round
    private static Bitmap getCircularBitmap(Bitmap bitmap) {
        Bitmap output;

        if (bitmap.getWidth() > bitmap.getHeight()) {
            output = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        } else {
            output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getWidth(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(output);

        final int color = 0xff424242;
        final Paint paint = new Paint();
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());

        float r = 0;

        if (bitmap.getWidth() > bitmap.getHeight()) {
            r = bitmap.getHeight() / 2;
        } else {
            r = bitmap.getWidth() / 2;
        }

        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        canvas.drawCircle(r, r, r, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }

    private void getMarkers(LatLng latestLocation) {
        CompletableFuture<List<OTMLocation>> places;
        float distance[] = new float[1];
        if (viewModel.getCenterOfLocations() == null) {
            viewModel.setCenterOfLocations(latestLocation); // Just put a default value, this should only happen at the beginning
        }
        Location.distanceBetween(latestLocation.latitude, latestLocation.longitude, viewModel.getCenterOfLocations().latitude, viewModel.getCenterOfLocations().longitude, distance);

        if (viewModel.getLocations() != null && distance[0] < 1000) {
            places = CompletableFuture.completedFuture(viewModel.getLocations());
        } else {
            mMap.clear();
            drawPositionMarker(latestLocation);

            LatLng upperLeft = new LatLng(latestLocation.latitude + LATITUDE_DIFF / 2, latestLocation.longitude - LONGITUDE_DIFF / 2);
            LatLng lowerRight = new LatLng(latestLocation.latitude - LATITUDE_DIFF / 2, latestLocation.longitude + LONGITUDE_DIFF / 2);
            places = otmProvider.getLocations(upperLeft, lowerRight).thenApply(x -> {
                viewModel.setCenterOfLocations(latestLocation);
                viewModel.setLocations(x);
                return x;
            });
        }
        places.thenAccept(x -> {
            for (OTMLocation location : x) {
                if (location.getName().isEmpty()) {
                    continue;
                }
                LatLng latLng = new LatLng(location.getCoordinates().getLat(), location.getCoordinates().getLon());
                Marker marker = mMap.addMarker(new MarkerOptions().position(latLng).title(location.getName()).snippet(String.join(", ", location.getKindsList())).icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE)));
                marker.setTag(location);
            }
        });
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        if (mMap != null) {
            outState.putParcelable("location", lastKnownLocation);
        }
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!isWifiAvailable) return;
        checkInternet();
    }

    private void getProfilePicture() {
        if (Profile.getActiveProfile() != null) {
            loadProfilePicture();
        } else {
            Database.getProfile(Authenticator.getCurrentUser().getUid()).whenComplete((profile, e) -> {
                if (e != null || profile == null) {
                    return;
                }

                Profile.setActiveProfile(profile);
                loadProfilePicture();
            });
        }

    }

    private void loadProfilePicture() {
        Picasso.get().load(Profile.getActiveProfile().getProfilePicture()).into(new Target() {
            @Override
            public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
                profilePicture = getCircularBitmap(bitmap);
                drawPositionMarker(viewModel.getCurrentLocation().getValue());
            }

            @Override
            public void onBitmapFailed(Exception e, Drawable errorDrawable) {
                Log.i("PICTURE", "FAILED TO LOAD PICTURE");
            }

            @Override
            public void onPrepareLoad(Drawable placeHolderDrawable) {
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            lastKnownLocation = savedInstanceState.getParcelable("location");
        }

        binding = FragmentMapsBinding.inflate(inflater, container, false);
        centerButton = binding.localisationButton;
        View mapView = binding.getRoot();

        checkInternet();
        if (!isWifiAvailable) return null;
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(getActivity());
        viewModel = new MapsViewModel();
        centerButton.setOnClickListener(v -> {
            mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(viewModel.getCurrentLocation().getValue(), DEFAULT_ZOOM));
        });

        otmProvider = new RetryingOTMProvider(new BasicOTMProvider());
        return mapView;
    }

    /**
     * Prompts the user for permission to use the device location.
     */
    private void getLocationPermission() {
        /*
         * Request location permission, so that we can get the location of the
         * device. The result of the permission request is handled by a callback,
         * onRequestPermissionsResult.
         */
        if (viewModel.isLocationPermissionGranted() || permissionRequest.hasPermission(getContext())) {
            viewModel.setIsLocationPermissionGranted(true);
            getDeviceLocation();
        } else {
            permissionRequest.askPermission(launcher);
        }
    }


    public void onRequestPermissionsResult(boolean grantResults) {
        viewModel.setIsLocationPermissionGranted(false);
        if (grantResults) {
            // If request is cancelled, the result arrays are empty.
            viewModel.setIsLocationPermissionGranted(true);
            getDeviceLocation();
        } else {
            this.getParentFragmentManager().beginTransaction().hide(this).show(unavailableFragment).setReorderingAllowed(true).commit();
            Toast.makeText(getContext(), "Please give access to your location to use this feature.", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SupportMapFragment mapFragment =
                (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
        if (mapFragment != null) {
            mapFragment.getMapAsync(callback);
        }
    }

    /**
     * Gets the current location of the device, and positions the map's camera.
     */
    private void getDeviceLocation() {
        /*
         * Get the best and most recent location of the device, which may be null in rare
         * cases when a location is not available.
         */
        try {
            if (viewModel.isLocationPermissionGranted()) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    locationRequest = new LocationRequest.Builder(10000).build();
                }
                LocationCallback locationCallback = new LocationCallback() {
                    @Override
                    public void onLocationResult(@NonNull LocationResult result) {
                        super.onLocationResult(result);
                        if (result != null) {
                            for (Location location : result.getLocations()) {
                                lastKnownLocation = location;
                            }
                            if (lastKnownLocation != null) {
                                viewModel.setCurrentLocation(new LatLng(lastKnownLocation.getLatitude(),
                                        lastKnownLocation.getLongitude()));
                                if (frame != null) {
                                    frame.setPosition(viewModel.getCurrentLocation().getValue());
                                }
                                if (profileMarker != null) {
                                    profileMarker.setPosition(viewModel.getCurrentLocation().getValue());
                                } else {
                                    getProfilePicture();
                                }
                            }

                            if(firstLocationUpdate) {
                                mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(viewModel.getCurrentLocation().getValue(), DEFAULT_ZOOM));
                                firstLocationUpdate = false;
                            }
                            getMarkers(viewModel.getCurrentLocation().getValue());
                        }

                    }
                };

                fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());
            }
        } catch (SecurityException e) {
            Log.e("Exception: %s", e.getMessage(), e);
        }
    }

    private void recenter(View view) {
        Toast.makeText(getContext(), "Recentering...", Toast.LENGTH_SHORT).show();
    }
}