bgabriel998/SoftwareDevProject

View on GitHub
app/src/main/java/ch/epfl/sdp/peakar/fragments/CameraFragment.java

Summary

Maintainability
A
0 mins
Test Coverage
C
73%
package ch.epfl.sdp.peakar.fragments;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.hardware.camera2.CameraAccessException;
import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager;

import com.google.common.util.concurrent.ListenableFuture;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import ch.epfl.sdp.peakar.R;
import ch.epfl.sdp.peakar.camera.CameraUiView;
import ch.epfl.sdp.peakar.camera.Compass;
import ch.epfl.sdp.peakar.camera.CompassListener;
import ch.epfl.sdp.peakar.database.Database;
import ch.epfl.sdp.peakar.points.POIPoint;
import ch.epfl.sdp.peakar.points.UserPoint;
import ch.epfl.sdp.peakar.user.score.UserScore;
import ch.epfl.sdp.peakar.user.services.Account;
import ch.epfl.sdp.peakar.user.services.AuthAccount;
import ch.epfl.sdp.peakar.user.services.AuthService;
import ch.epfl.sdp.peakar.utils.CameraUtilities;
import ch.epfl.sdp.peakar.utils.StorageHandler;

import static ch.epfl.sdp.peakar.general.MainActivity.lastFragmentIndex;
import static ch.epfl.sdp.peakar.utils.MenuBarHandlerFragments.updateSelectedIcon;
import static ch.epfl.sdp.peakar.utils.MainPagerAdapter.CAMERA_FRAGMENT_INDEX;
import static ch.epfl.sdp.peakar.utils.PermissionUtilities.createAllPermissionListener;
import static ch.epfl.sdp.peakar.utils.PermissionUtilities.hasCameraPermission;
import static ch.epfl.sdp.peakar.utils.StatusBarHandlerFragments.StatusBarTransparentBlack;
import static ch.epfl.sdp.peakar.utils.TopBarHandlerFragments.setupTransparentTopBar;

/**
 * A {@link Fragment} subclass that represents the camera-preview.
 * Use the {@link CameraFragment#newInstance} factory method to
 * create an instance of this fragment.
 * See: https://github.com/android/camera-samples/blob/main/CameraXBasic
 */
public class CameraFragment extends Fragment{

    private PreviewView previewView;
    private ProcessCameraProvider cameraProvider;
    private ImageCapture imageCapture;
    private ExecutorService cameraExecutor;
    private Context context;

    private int previewDisplayId = -1;

    private DisplayManager displayManager;

    private DisplayManager.DisplayListener displayListener;

    private boolean returnToFragment;


    private static final int FLASH_TIME_MS = 5;
    //Widgets
    private CameraUiView cameraUiView;
    private TextView headingHorizontal;
    private TextView headingVertical;
    private TextView fovHorVer;
    private TextView userLocation;
    private TextView userAltitude;
    private Compass compass;
    private View flash;
    private TextView displayModeText;

    //SharedPreferences
    private SharedPreferences sharedPref;

    private boolean showDevOptions;

    private static final String DISPLAY_ALL_POIS = "0";

    private ImageView compassMiniature;
    private TextView headingCompass;

    private ConstraintLayout container;

    private MultiplePermissionsListener allPermissionsListener;

    /**
     * Constructor for the CameraPreview
     * Is required to be empty for the fragments
     */
    public CameraFragment() {}

    /**
     * Use this factory method to create a new instance of
     * this fragment.
     *
     * @return A new instance of fragment CameraPreview.
     */
    public static CameraFragment newInstance() {
        return new CameraFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        returnToFragment = false;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_camera, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        container = (ConstraintLayout) view;
        previewView = container.findViewById(R.id.cameraPreviewFragment);

        //Initialize background executor
        cameraExecutor = Executors.newSingleThreadExecutor();

        initialiseListeners();

        //Configure context
        context = getContext();

        //Wait for the view to be properly laid out
        previewView.post(() -> {
            previewDisplayId = previewView.getDisplay().getDisplayId();
            setUpCamera();
            setUpUI();
            Dexter.withContext(requireContext())
                    .withPermissions(
                            Manifest.permission.ACCESS_FINE_LOCATION,
                            Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA
                    ).withListener(allPermissionsListener)
                    .check();
        });
    }

    /**
     * Initialises the listeners for the camera fragment
     */
    private void initialiseListeners() {
        //Register display listener
        displayManager.registerDisplayListener(displayListener, null);

        container.findViewById(R.id.takePicture).setOnClickListener(this::takePictureListener);
        container.findViewById(R.id.compassMiniature).setOnClickListener(this::switchDisplayCompass);
        container.findViewById(R.id.switchDisplayPOIs).setOnClickListener(this::switchDisplayPOIMode);
        container.findViewById(R.id.openSettingsButton).setOnTouchListener((v, event) -> openSettings());
        allPermissionsListener = createAllPermissionListener(requireContext(), container);
    }

    /**
     * Sets up the UI for the camera fragment and starts the compass
     */
    private void setUpUI() {
        sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext());

        displayModeText = container.findViewById(R.id.textDisplayPOImode);
        String mode = sharedPref.getString(getResources().getString(R.string.displayPOIs_key), DISPLAY_ALL_POIS);
        displayModeText.setText(mode);

        showDevOptions = sharedPref.getBoolean(getResources().getString(R.string.devOptions_key), false);
        displayDeveloperOptions(showDevOptions);

        cameraUiView = container.findViewById(R.id.compass);
        flash = container.findViewById(R.id.take_picture_flash);

        //Setup the compass
        startCompass();
    }

    /**
     * startCompass creates the compass and initializes the compass listener
     */
    public void startCompass() {
        //Get the fov of the camera
        Pair<Float, Float> cameraFieldOfView = new Pair<>(0f, 0f);
        try {
            cameraFieldOfView = CameraUtilities.getFieldOfView(requireContext());
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        if (showDevOptions && cameraFieldOfView!=null) {
            //Set text for demo/debug
            fovHorVer.setText(String.format(Locale.ENGLISH, "%.1f°, %.1f°", cameraFieldOfView.first, cameraFieldOfView.second));
            UserPoint userPoint = UserPoint.getInstance(getContext());
            userLocation.setText(String.format(Locale.ENGLISH, "%.4f °, %.4f °", userPoint.getLatitude(), userPoint.getLongitude()));
            userAltitude.setText(String.format(Locale.ENGLISH, "%.1f m", userPoint.getAltitude()));
        }

        cameraUiView.setRange(cameraFieldOfView);

        //Create new compass
        compass = new Compass(requireContext());

        compassMiniature = container.findViewById(R.id.compassMiniature);
        headingCompass = container.findViewById(R.id.headingCompass);

        //Bind the compassListener with the compass
        compass.setListener(getCompassListener());
    }

    /**
     * getCompassListener returns a CompassListener which updates the compass view and the textviews
     * with the actual heading
     *
     * @return CompassListener for the compass
     */
    private CompassListener getCompassListener() {
        return (heading, headingV) -> {
            //Update the compass when the heading changes
            cameraUiView.setDegrees(heading, headingV);
            compassMiniature.setRotation(-1*heading);
            //Update the textviews with the new headings
            int headingInt = (int)heading == 360 ? 0 : (int)heading;
            headingCompass.setText(String.format(Locale.ENGLISH, "%d°", headingInt));
            headingHorizontal.setText(String.format(Locale.ENGLISH, "%.1f °", heading));
            headingVertical.setText(String.format(Locale.ENGLISH, "%.1f °", headingV));
        };
    }

    /**
     * Gets the currently logged in user account and adds the mountains to the discovered Peaks
     */
    private void addDiscoveredPOIsToDatabase(){
        if(cameraUiView==null) return;

        List<POIPoint> discoveredPOIPoints = cameraUiView.getDiscoveredPOIPoints();
        AuthService service = AuthService.getInstance();
        AuthAccount acc = service.getAuthAccount();
        if(acc != null && !discoveredPOIPoints.isEmpty() && Database.getInstance().isOnline()){
            if(!acc.getUsername().equals(Account.USERNAME_BEFORE_REGISTRATION)){
                UserScore userScore = new UserScore(getContext());
                userScore.updateUserScoreAndDiscoveredPeaks((ArrayList<POIPoint>) discoveredPOIPoints);
            }
            else{
                Toast.makeText(getContext(), getResources().getString(R.string.setUsername), Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * Callback for the takePicture ImageButton takes two pictures, one of the camera and one with the UI
     *
     * @param view ImageButton
     */
    public void takePictureListener(View view) {
        flash.setVisibility(View.VISIBLE);
        //Take a picture with the camera without the UI
        takePicture();
        //Create a bitmap of the camera preview
        Bitmap cameraBitmap = previewView.getBitmap();
        //Create a bitmap of the compass-view
        Bitmap compassBitmap = cameraUiView.getBitmap();
        //Combine the two bitmaps
        assert cameraBitmap != null;
        Bitmap bitmap = CameraUtilities.combineBitmaps(cameraBitmap, compassBitmap);
        //Store the bitmap on the user device
        try {
            StorageHandler.storeBitmap(getContext(), bitmap);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //Set visibility to invisible again after FLASH_TIME_MS
        flash.postDelayed(() -> flash.setVisibility(View.GONE), FLASH_TIME_MS);
    }

    /**
     * Displays the developer options (horizontal and vertical heading and the camera fov) if
     * devOption is true.
     * @param devOption Boolean, to determine if the developer options are shown or not
     */
    private void displayDeveloperOptions(boolean devOption) {
        headingHorizontal = container.findViewById(R.id.headingHorizontal);
        headingVertical = container.findViewById(R.id.headingVertical);
        fovHorVer = container.findViewById(R.id.fovHorVer);
        userLocation = container.findViewById(R.id.userLocation);
        userAltitude = container.findViewById(R.id.userAltitude);

        headingHorizontal.setVisibility(devOption ? View.VISIBLE : View.GONE);
        headingVertical.setVisibility(devOption ? View.VISIBLE : View.GONE);
        fovHorVer.setVisibility(devOption ? View.VISIBLE : View.GONE);
        userLocation.setVisibility(devOption ? View.VISIBLE : View.GONE);
        userAltitude.setVisibility(devOption ? View.VISIBLE : View.GONE);
    }

    /**
     * Callback for the switchDisplayPOIs ImageButton, iterates over the different representation modes:
     * 1. Display all POIs
     * 2. Display only POIs in line of sight
     * 3. Display only POIS out of line of sight
     *
     * @param view ImageButton
     */
    public void switchDisplayPOIMode(View view) {
        flash.setVisibility(View.VISIBLE);
        String displayPOIsKey = getResources().getString(R.string.displayPOIs_key);
        String mode = sharedPref.getString(displayPOIsKey, DISPLAY_ALL_POIS);
        int actualMode = Integer.parseInt(mode);
        int newMode = (actualMode + 1) % 3;
        displayModeText.setText(String.valueOf(newMode));
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putString(displayPOIsKey, "" + newMode);
        editor.apply();
        flash.postDelayed(() -> flash.setVisibility(View.GONE), FLASH_TIME_MS);
    }

    /**
     * Callback for the switchDisplayPOIs ImageButton, if true, then display the compass
     *
     * @param view CompassButton
     */
    public void switchDisplayCompass(View view) {
        String displayCompassString = getResources().getString(R.string.displayCompass_key);
        boolean displayCompass = sharedPref.getBoolean(displayCompassString, false);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putBoolean(displayCompassString, !displayCompass);
        editor.apply();
    }

    @Override
    public void onResume() {
        super.onResume();
        //Remove the fragment from the stack if it is already contained and then push it on the stack
        if(lastFragmentIndex.contains(CAMERA_FRAGMENT_INDEX)) {
            lastFragmentIndex.remove((Object)CAMERA_FRAGMENT_INDEX);
        }
        lastFragmentIndex.push(CAMERA_FRAGMENT_INDEX);
        if(returnToFragment){
            initFragment();
            startCompass();
            showDevOptions = sharedPref.getBoolean(getResources().getString(R.string.devOptions_key), false);
            displayDeveloperOptions(showDevOptions);
            setUpUI();
        }
        else{
            initFragment();
            returnToFragment = true;
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        addDiscoveredPOIsToDatabase();
    }

    /**
     * Initialises the fragment
     */
    private void initFragment() {
        requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
        updateSelectedIcon(this);
        StatusBarTransparentBlack(this);
        setupTransparentTopBar(this, R.color.White);

        container.findViewById(R.id.permissionRequestLayout).setVisibility(!hasCameraPermission(requireContext()) ? View.VISIBLE : View.GONE);
    }

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);

        displayManager = (DisplayManager) requireContext().getSystemService(Context.DISPLAY_SERVICE);

        displayListener = new DisplayManager.DisplayListener() {
            @Override
            public void onDisplayAdded(int displayId) {}

            @Override
            public void onDisplayRemoved(int displayId) {}

            @Override
            public void onDisplayChanged(int displayId) {
                if(displayId == previewDisplayId){
                    imageCapture.setTargetRotation(requireView().getDisplay().getRotation());
                }
            }
        };
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //Unbind use-cases before exiting
        if(cameraProvider!=null) cameraProvider.unbindAll();
        // Shut down our background executor
        if(cameraExecutor!=null) cameraExecutor.shutdown();
        if(compass!=null) compass.stop();
        if(displayManager!=null) displayManager.unregisterDisplayListener(displayListener);
    }

    /**
     *  Setup cameraProvider and call bindPreview
     */
    private void setUpCamera(){
        //ProcessCameraProvider: Used to bind the lifecycle of cameras
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);

        cameraProviderFuture.addListener(() -> {
            //CameraProvider
            try {
                cameraProvider = cameraProviderFuture.get();
                bindPreview(cameraProvider);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, ContextCompat.getMainExecutor(context));
    }

    /**
     * Declare and bind preview and analysis use cases
     * @param cameraProvider used to bind the lifecycle of the camera
     */
    private void bindPreview(ProcessCameraProvider cameraProvider) {
        //Get screen metrics
        DisplayMetrics displayMetrics = new DisplayMetrics();
        previewView.getDisplay().getRealMetrics(displayMetrics);

        //Calculate aspectRatio
        int screenAspectRatio = CameraUtilities.aspectRatio(displayMetrics.widthPixels, displayMetrics.heightPixels);

        //Get screen rotation
        int rotation = previewView.getDisplay().getRotation();

        //CameraSelector
        CameraSelector cameraSelector = new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();

        //preview
        Preview preview = new Preview.Builder().setTargetAspectRatio(screenAspectRatio).setTargetRotation(rotation).build();

        // ImageCapture
        imageCapture = new ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).
                setTargetAspectRatio(screenAspectRatio).setTargetRotation(rotation).build();

        //Unbind use-cases before rebinding
        cameraProvider.unbindAll();

        //Bind use cases to camera
        cameraProvider.bindToLifecycle((LifecycleOwner) context, cameraSelector, preview, imageCapture);

        //Attach the viewfinder's surface provider to preview
        preview.setSurfaceProvider(previewView.getSurfaceProvider());
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        //Redraw the cameraUI
        setUpCamera();
    }

    /**
     * Takes a picture of the camera-preview without the canvas drawn
     */
    public void takePicture() {
        //Create the file
        File photoFile = StorageHandler.createPhotoFile(context);

        //Configure output options
        ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
                photoFile).build();

        //Take the picture
        imageCapture.takePicture(outputOptions, cameraExecutor, new ImageCapture.OnImageSavedCallback() {
            @Override
            public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
            }

            @Override
            public void onError(@NonNull ImageCaptureException exception) {
            }
        });
    }

    /**
     * Opens the settings in android of the application
     * @return true if succesfull
     */
    private boolean openSettings(){
        Context context = requireContext();
        Intent i = new Intent();
        i.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        i.addCategory(Intent.CATEGORY_DEFAULT);
        i.setData(Uri.parse("package:" + context.getPackageName()));
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
        i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        context.startActivity(i);
        return true;
    }
}