TiagoMSSantos/MobileRT

View on GitHub
app/src/main/java/puscas/mobilertapp/MainActivity.java

Summary

Maintainability
F
3 days
Test Coverage
package puscas.mobilertapp;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ConfigurationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;

import java.io.File;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;

import javax.annotation.Nonnull;

import java8.util.Optional;
import java8.util.stream.IntStreams;
import java8.util.stream.StreamSupport;
import kotlin.Pair;
import puscas.mobilertapp.configs.Config;
import puscas.mobilertapp.configs.ConfigResolution;
import puscas.mobilertapp.configs.ConfigSamples;
import puscas.mobilertapp.constants.Accelerator;
import puscas.mobilertapp.constants.ConstantsMethods;
import puscas.mobilertapp.constants.ConstantsToast;
import puscas.mobilertapp.constants.ConstantsUI;
import puscas.mobilertapp.constants.Scene;
import puscas.mobilertapp.constants.Shader;
import puscas.mobilertapp.constants.State;
import puscas.mobilertapp.exceptions.FailureException;
import puscas.mobilertapp.utils.Utils;
import puscas.mobilertapp.utils.UtilsContext;
import puscas.mobilertapp.utils.UtilsGL;
import puscas.mobilertapp.utils.UtilsLogging;

/**
 * The main {@link Activity} for the Android User Interface.
 */
public final class MainActivity extends Activity {

    /**
     * Logger for this class.
     */
    private static final Logger logger = Logger.getLogger(MainActivity.class.getSimpleName());

    /**
     * The request code for the new {@link Activity} to open an OBJ file.
     */
    private static final int OPEN_FILE_REQUEST_CODE = 1;

    /**
     * The OpenGL ES version required to run this application.
     */
    private static final int REQUIRED_OPENGL_VERSION = 0x20000;

    /**
     * The current active instance of {@link MainActivity}.
     * <p>
     * This is probably a very bad idea but got not better solution to simplify the showing of
     * custom messages on the UI so the user can understand better when the system crashes.
     *
     * @implNote This is necessary for the {@link MainActivity#showUiMessage(String)} method to
     * have the application {@link Context} and so the method can be static and used anywhere in the
     * codebase.
     */
    @SuppressWarnings({"StaticFieldLeak"})
    private static Activity currentInstance = null;

    /*
     ***********************************************************************
     * Private instance fields
     ***********************************************************************
     */

    /**
     * The custom {@link GLSurfaceView} for displaying OpenGL rendering.
     */
    private DrawView drawView = null;

    /**
     * The {@link NumberPicker} to select the scene to render.
     */
    private NumberPicker pickerScene = null;

    /**
     * The {@link NumberPicker} to select the shader.
     */
    private NumberPicker pickerShader = null;

    /**
     * The {@link NumberPicker} to select the number of threads.
     */
    private NumberPicker pickerThreads = null;

    /**
     * The {@link NumberPicker} to select the accelerator.
     */
    private NumberPicker pickerAccelerator = null;

    /**
     * The {@link NumberPicker} to select the number of samples per pixel.
     */
    private NumberPicker pickerSamplesPixel = null;

    /**
     * The {@link NumberPicker} to select the number of samples per light.
     */
    private NumberPicker pickerSamplesLight = null;

    /**
     * The {@link NumberPicker} to select the desired resolution for the
     * rendered image.
     */
    private NumberPicker pickerResolutions = null;

    /**
     * The {@link CheckBox} to select whether should render a preview of the
     * scene (rasterize) or not.
     */
    private CheckBox checkBoxRasterize = null;

    /**
     * The path to a directory containing the OBJ and MTL files of a scene.
     */
    private String sceneFilePath = null;

    /**
     * Loads the MobileRT native library.
     */
    private static void loadMobileRT() {
        System.loadLibrary("MobileRT");
        System.loadLibrary("Components");
        System.loadLibrary("AppMobileRT");
    }

    /**
     * Helper method that calls the UI thread to update the UI with a custom message.
     *
     * @param message The message to show on the UI.
     * @implNote Only the UI thread can update the UI with messages by using {@link Toast}.
     */
    public static void showUiMessage(@NonNull final String message) {
        currentInstance.runOnUiThread(() -> {
            logger.info("showUiMessage: " + message);
            Toast.makeText(currentInstance.getApplicationContext(), message, Toast.LENGTH_LONG)
                .show();
        });
    }

    /**
     * Helper method that calls the UI thread to update the render button on the UI.
     *
     * @implNote Only the UI thread can update the state of the render button.
     */
    public static void resetRenderButton() {
        currentInstance.runOnUiThread(() -> {
            logger.info("updateRenderButton");
            final Button renderButton = currentInstance.findViewById(R.id.renderButton);
            renderButton.setText(R.string.render);
        });
    }

    /**
     * Helper method that checks if the supported OpenGL ES version is
     * the version 2 or greater.
     *
     * @param activityManager The {@link ActivityManager} which contains the
     *                        {@link ConfigurationInfo}.
     */
    private static void checksOpenGlVersion(final ActivityManager activityManager) {
        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
        final boolean supportES2 = (configurationInfo.reqGlEsVersion >= REQUIRED_OPENGL_VERSION);

        if (!supportES2 || !UtilsGL.checkGL20Support()) {
            final String msg = "Your device doesn't support ES 2. ("
                + configurationInfo.reqGlEsVersion + ')';
            throw new FailureException(msg);
        }
    }

    /**
     * Initializes a {@link NumberPicker}.
     *
     * @param numberPicker The {@link NumberPicker} to initialize.
     * @param defaultValue The default value to put in the {@link NumberPicker}.
     * @param names        The values to be displayed in the {@link NumberPicker}.
     */
    private static void initializePicker(final NumberPicker numberPicker,
                                         final int defaultValue,
                                         final String[] names) {
        try {
            final int minValue = Integer.parseInt(names[0]);
            numberPicker.setMinValue(minValue);
            numberPicker.setMaxValue(names.length);
        } catch (final NumberFormatException ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#initializePicker");
            numberPicker.setMinValue(0);
            numberPicker.setMaxValue(names.length - 1);
        }

        numberPicker.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        numberPicker.setValue(defaultValue);
        numberPicker.setDisplayedValues(names);

        // TODO: For Android API < 15, this method crashes, so it's necessary to investigate it.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            numberPicker.setWrapSelectorWheel(true);
        }
    }

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        try {
            loadMobileRT();
        } catch (final Exception ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#onCreate");
            throw new FailureException(ex);
        }

        setCurrentInstance();
        super.onCreate(savedInstanceState);
        logger.info("onCreate start");

        setContentView(R.layout.activity_main);
        initializeViews();

        final TextView textView = findViewById(R.id.timeText);
        final Button renderButton = findViewById(R.id.renderButton);
        renderButton.setOnClickListener(this::startRender);

        final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

        checksOpenGlVersion(activityManager);
        setupRenderer(textView, renderButton);

        final Optional<Bundle> bundle = Optional.ofNullable(savedInstanceState);
        initializePickers(bundle);
        initializeCheckBoxRasterize(bundle.map(x -> x.getBoolean(ConstantsUI.CHECK_BOX_RASTERIZE))
            .orElse(true));

        UtilsContext.checksStoragePermission(this);

        logger.info("onCreate finish");
    }

    @Override
    protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        logger.info("onRestoreInstanceState");

        final int scene = savedInstanceState.getInt(ConstantsUI.PICKER_SCENE);
        final int shader = savedInstanceState.getInt(ConstantsUI.PICKER_SHADER);
        final int threads = savedInstanceState.getInt(ConstantsUI.PICKER_THREADS);
        final int accelerator = savedInstanceState.getInt(ConstantsUI.PICKER_ACCELERATOR);
        final int samplesPixel = savedInstanceState.getInt(ConstantsUI.PICKER_SAMPLES_PIXEL);
        final int samplesLight = savedInstanceState.getInt(ConstantsUI.PICKER_SAMPLES_LIGHT);
        final int sizes = savedInstanceState.getInt(ConstantsUI.PICKER_SIZE);
        final boolean rasterize = savedInstanceState.getBoolean(ConstantsUI.CHECK_BOX_RASTERIZE);

        this.pickerScene.setValue(scene);
        this.pickerShader.setValue(shader);
        this.pickerThreads.setValue(threads);
        this.pickerAccelerator.setValue(accelerator);
        this.pickerSamplesPixel.setValue(samplesPixel);
        this.pickerSamplesLight.setValue(samplesLight);
        this.pickerResolutions.setValue(sizes);
        this.checkBoxRasterize.setChecked(rasterize);
    }

    @Override
    protected void onResume() {
        super.onResume();
        logger.info("onResume start");

        this.drawView.onResume();
        this.drawView.setVisibility(View.VISIBLE);
        logger.info("onResume end");
    }

    @Override
    protected void onPostResume() {
        super.onPostResume();
        logger.info("onPostResume start");

        // Start rendering if its resuming from selecting a scene via an
        // external file manager and it was selected a file with a scene to
        // render.
        // This method should be automatically called after `onActivityResult`.
        if (!Strings.isNullOrEmpty(this.sceneFilePath)) {
            try {
                startRender(this.sceneFilePath);
            } catch (final Exception ex) {
                UtilsLogging.logThrowable(ex, "MainActivity#onPostResume");
                showUiMessage(Objects.requireNonNull(ex.getMessage()));
            }
        }
        logger.info("onPostResume end");
    }

    @Override
    protected void onSaveInstanceState(@NonNull final Bundle outState) {
        super.onSaveInstanceState(outState);
        logger.info("onSaveInstanceState");

        outState.putInt(ConstantsUI.PICKER_SCENE, this.pickerScene.getValue());
        outState.putInt(ConstantsUI.PICKER_SHADER, this.pickerShader.getValue());
        outState.putInt(ConstantsUI.PICKER_THREADS, this.pickerThreads.getValue());
        outState.putInt(ConstantsUI.PICKER_ACCELERATOR, this.pickerAccelerator.getValue());
        outState.putInt(ConstantsUI.PICKER_SAMPLES_PIXEL, this.pickerSamplesPixel.getValue());
        outState.putInt(ConstantsUI.PICKER_SAMPLES_LIGHT, this.pickerSamplesLight.getValue());
        outState.putInt(ConstantsUI.PICKER_SIZE, this.pickerResolutions.getValue());
        outState.putBoolean(ConstantsUI.CHECK_BOX_RASTERIZE, this.checkBoxRasterize.isChecked());

        this.drawView.finishRenderer();
    }

    @Override
    protected void onPause() {
        super.onPause();
        logger.info("onPause");

        Utils.handleInterruption("MainActivity#onPause");

        this.drawView.setPreserveEGLContextOnPause(true);
        this.drawView.onPause();
        this.drawView.setVisibility(View.INVISIBLE);
        this.sceneFilePath = null;

        final String message = "onPause" + ConstantsMethods.FINISHED;
        logger.info(message);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        logger.info(ConstantsMethods.ON_DESTROY);

        MainActivity.resetErrno(); // Avoid 'bad file descriptor' error.
        this.drawView.onDetachedFromWindow();
        this.drawView.setVisibility(View.INVISIBLE);

        final String message = ConstantsMethods.ON_DESTROY + ConstantsMethods.FINISHED;
        logger.info(message);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        logger.info(ConstantsMethods.ON_DETACHED_FROM_WINDOW);

        this.drawView.onDetachedFromWindow();

        final String message = ConstantsMethods.ON_DETACHED_FROM_WINDOW + ConstantsMethods.FINISHED;
        logger.info(message);
    }

    @Override
    public void onRequestPermissionsResult(final int requestCode,
                                           @NonNull final String[] permissions,
                                           @NonNull final int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        logger.info("onRequestPermissionsResult");

        if (permissions.length > 0) {
            logger.info("Requested permissions: " + Arrays.toString(permissions));

            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted. Continue the action or workflow
                // in your app.
                logger.info("Permission granted!");
            }  else {
                // Explain to the user that the feature is unavailable because
                // the features requires a permission that the user has denied.
                // At the same time, respect the user's decision. Don't link to
                // system settings in an effort to convince the user to change
                // their decision.
                logger.severe("Permission NOT granted");
            }
            // Other 'case' lines to check for other
            // permissions this app might request.
        }
    }

    /**
     * Helper method which creates an {@link Intent} with the goal to ask Android System to read
     * some files by an external file manager.
     *
     * @param packageName The name of the Android package.
     * @return The {@link Intent} to load files.
     */
    public static Intent createIntentToLoadFiles(@NonNull final String packageName) {
        final String message = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
            ? "Requesting Intent to load a file for: '" + packageName + "' (has shared/external file access: " + Environment.isExternalStorageManager() + ")"
            : "Requesting Intent to load a file for: '" + packageName + "'";
        logger.info(message);
        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.putExtra(Intent.EXTRA_TITLE, "Select an OBJ file to load.");
        intent.setType("*" + ConstantsUI.FILE_SEPARATOR + "*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        }
        return intent;
    }

    @Override
    protected void onActivityResult(final int requestCode,
                                    final int resultCode,
                                    @Nullable final Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        logger.info("onActivityResult requestCode: " + requestCode + ", resultCode: " + resultCode);

        try {
            if (data != null && Objects.equals(resultCode, Activity.RESULT_OK)) {
                if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && data.getClipData() != null) {
                    final ClipData clipData = data.getClipData();
                    final int numFiles = clipData.getItemCount();
                    logger.info("Will read every selected file: " + numFiles);
                    for (int i = 0; i < numFiles; ++i) {
                        final ClipData.Item item = clipData.getItemAt(i);
                        final Uri uri = item.getUri();
                        if (uri == null) {
                            throw new FailureException("There is no URI to a File! [" + i + "]");
                        }
                        final String filePath = getPathFromFile(uri);
                        readFile(uri);
                        if (filePath.endsWith(".obj")) {
                            this.sceneFilePath = filePath;
                        }
                    }
                } else {
                    logger.info("Will read every file in a path.");
                    final Uri uri = data.getData();
                    if (uri == null || uri.getPath() == null) {
                        throw new FailureException("There is no URI to a File!");
                    }
                    final String filePath = getPathFromFile(uri);
                    if (filePath.endsWith(".obj")) {
                        this.sceneFilePath = filePath;
                    }
                    final File[] files = getFilesFromDirectory(uri);
                    for(final File file : files) {
                        readFile(Uri.fromFile(file));
                    }
                }
            } else {
                logger.severe("There is no URI to a File!");
                this.sceneFilePath = null;
            }
        } catch (final Exception ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#onActivityResult");
            MainActivity.showUiMessage(ConstantsToast.COULD_NOT_RENDER_THE_SCENE + ex.getMessage());
        }
        logger.info("onActivityResult finished");
    }

    /**
     * Gets the files from the directory path received via parameter.
     * <p>
     * If the provided path is to a file instead of a directory, then this method lists all files
     * inside the directory containing that file.
     *
     * @param uri The {@link Uri} pointing to a path.
     * @return An array of {@link File}s that are in the desired path.
     */
    @NonNull
    private static File[] getFilesFromDirectory(@Nonnull final Uri uri) {
        final File baseFile = new File(Objects.requireNonNull(uri.getPath()));
        final File[] files;
        if (baseFile.isDirectory()) {
            files = baseFile.listFiles();
        } else {
            files = new File(Objects.requireNonNull(baseFile.getParent())).listFiles();
        }
        if (files == null) {
            throw new FailureException("It couldn't list the files in the selected path. Are you sure the necessary permissions were given?");
        }
        return files;
    }

    /**
     * Starts the rendering process when the user clicks the render
     * {@link Button}.
     *
     * @param view The view of the {@link Activity}.
     */
    public void startRender(@NonNull final View view) {
        try {
            final String message = ConstantsMethods.START_RENDER + ": " + view;
            logger.info(message);

            final State state = this.drawView.getRayTracerState();
            if (state == State.BUSY) {
                this.drawView.stopDrawing();
            } else {
                startRenderScene();
            }

            final String messageFinished = ConstantsMethods.START_RENDER + ConstantsMethods.FINISHED;
            logger.info(messageFinished);
        } catch (final Exception ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#startRender");
            MainActivity.showUiMessage(ConstantsToast.COULD_NOT_RENDER_THE_SCENE + ex.getMessage());
        }
    }

    /**
     * Helper method which starts or stops the rendering process.
     *
     * @param scenePath The path to a directory containing the OBJ and MTL files
     *                  of a scene to render.
     */
    private void startRender(@NonNull final String scenePath) {
        logger.info(ConstantsMethods.START_RENDER);

        final Config config = createConfigFromUI(scenePath);
        this.drawView.renderScene(config);

        final String message = ConstantsMethods.START_RENDER + ConstantsMethods.FINISHED;
        logger.info(message);
    }

    /**
     * Helper method which starts the rendering process.
     */
    private void startRenderScene() {
        if (Scene.values()[this.pickerScene.getValue()] == Scene.OBJ) {
            callFileManager();
        } else {
            startRender("");
        }
    }

    /**
     * Auxiliary method to readjust the width and height of the image by
     * rounding down the value to a multiple of the number of tiles in the
     * Ray Tracer engine.
     *
     * @param size The value to be rounded down to a multiple of the number of
     *             tiles in the Ray Tracer engine.
     * @return The highest value that is smaller than the size passed by
     *     parameter and is a multiple of the number of tiles.
     */
    private native int rtResize(int size);

    /**
     * Resets the C `errno` error code to 0.
     */
    public static native void resetErrno();

    /**
     * Reads a file from a file descriptor natively.
     * It uses C functions in JNI to read the file.
     *
     * @param fileDescriptor The file descriptor.
     * @param filePathSize   The size of the file in bytes.
     * @param filePath       The path to a file to be used by MobileRT, which should be either:<br/>
     *                       * an OBJ file<br/>
     *                       * a MTL file<br/>
     *                       * a CAM file<br/>
     *                       * a texture file<br/>
     */
    private native void readFile(int fileDescriptor, long filePathSize, String filePath);

    /**
     * Gets the path of a file that was loaded with an external file manager.
     * <br>
     * This method basically translates an {@link Uri} path to a {@link String}
     * but also tries to be compatible with any device / emulator available.
     *
     * @param uri The URI reference for the file.
     * @return The path to the file.
     */
    @NonNull
    private String getPathFromFile(@NonNull final Uri uri) {
        logger.info("Parsing path:" + Arrays.toString(uri.getPathSegments().toArray()));
        validatePathIsAccessible(uri);

        final String filePath = StreamSupport.stream(uri.getPathSegments())
            .skip(1L)
            .reduce("", (accumulator, segment) -> accumulator + ConstantsUI.FILE_SEPARATOR + segment);
        final boolean externalSDCardPath =
                uri.getPathSegments().get(0).matches("sdcard")
            || (uri.getPathSegments().size() > 1 && uri.getPathSegments().get(1).matches("^([A-Za-z0-9]){4}-([A-Za-z0-9]){4}:.+$"))
            || (uri.getPathSegments().size() > 1 && uri.getPathSegments().get(1).matches("^([A-Za-z0-9]){4}-([A-Za-z0-9]){4}$"))
            || (uri.getPathSegments().size() > 2 && uri.getPathSegments().get(2).matches("^([A-Za-z0-9]){4}-([A-Za-z0-9]){4}$"))
            || (uri.getPathSegments().get(0).matches("^mnt$") && uri.getPathSegments().get(1).matches("^sdcard$"))
            || (uri.getPathSegments().get(0).matches("^storage$") && uri.getPathSegments().get(1).matches("^sdcard$"))
            || (uri.getPathSegments().get(0).matches("^storage$") && uri.getPathSegments().get(1).matches("^emulated$") && uri.getPathSegments().get(2).matches("^0$"))
            || filePath.contains(Environment.getExternalStorageDirectory().getAbsolutePath());

        final String devicePath;
        if (externalSDCardPath) {
            devicePath = UtilsContext.getSdCardPath(this);
        } else {
            devicePath = UtilsContext.getInternalStoragePath(this);
        }

        // SDK API 30 looks like to get the path to the file properly without having to get the
        // SD card path and prefix with it.
        final String cleanedFilePath = cleanFilePath(filePath);
        if (cleanedFilePath.startsWith(devicePath)) {
            return cleanedFilePath;
        }
        if (cleanedFilePath.startsWith(ConstantsUI.FILE_SEPARATOR + "emulated" + ConstantsUI.FILE_SEPARATOR + "0" + ConstantsUI.FILE_SEPARATOR)) {
            return ConstantsUI.FILE_SEPARATOR + "storage" + cleanedFilePath;
        }

        return devicePath + cleanedFilePath;
    }

    /**
     * Cleans the file path, by removing the prefix of the path that points to a local storage or
     * to an external SD card.
     *
     * @param filePath The path a file.
     * @return The relative path to the file, without the path prefix to the storage.
     */
    @NonNull
    private static String cleanFilePath(final String filePath) {
        final int removeIndex = filePath.indexOf(ConstantsUI.PATH_SEPARATOR);
        final String startFilePath = removeIndex >= 0 ? filePath.substring(removeIndex) : filePath;
        final String escapedFileSeparator = Objects.equals(ConstantsUI.FILE_SEPARATOR, "\\")
            ? ConstantsUI.FILE_SEPARATOR + ConstantsUI.FILE_SEPARATOR
            : ConstantsUI.FILE_SEPARATOR;
        String cleanedFilePath = startFilePath.replace(ConstantsUI.PATH_SEPARATOR, ConstantsUI.FILE_SEPARATOR);
        cleanedFilePath = cleanedFilePath.replaceFirst("^" + escapedFileSeparator + "sdcard" + escapedFileSeparator, escapedFileSeparator);
        cleanedFilePath = cleanedFilePath.replaceFirst("^" + escapedFileSeparator + "([A-Za-z0-9]){4}-([A-Za-z0-9]){4}" + escapedFileSeparator, escapedFileSeparator);
        cleanedFilePath = cleanedFilePath.replaceFirst("^" + escapedFileSeparator + "local" + escapedFileSeparator + "tmp" + escapedFileSeparator, escapedFileSeparator);
        return cleanedFilePath;
    }

    /**
     * Validates that the user selected path in {@link Uri} can be read safely.
     * If the {@link Uri path} that the user selected can be dangerous, like '/data', '/system',
     * then a {@link SecurityException} is thrown.
     *
     * @param uri The {@link Uri} reference for the file.
     */
    private void validatePathIsAccessible(@NonNull final Uri uri) {
        logger.info("validatePathIsAccessible");
        final String path = Objects.requireNonNull(uri.getPath());

        final String escapedFileSeparator = Objects.equals(ConstantsUI.FILE_SEPARATOR, "\\")
                ? ConstantsUI.FILE_SEPARATOR + ConstantsUI.FILE_SEPARATOR
                : ConstantsUI.FILE_SEPARATOR;
        boolean externalStorage1 = path.matches("^" + escapedFileSeparator + "document" + escapedFileSeparator + "([A-Za-z0-9]){4}-([A-Za-z0-9]){4}:.+$");
        boolean externalStorage2 = path.matches("^" + escapedFileSeparator + "mnt" + escapedFileSeparator + "sdcard" + escapedFileSeparator + ".+$");
        boolean externalStorage3 = path.matches("^" + escapedFileSeparator + "storage" + escapedFileSeparator + "emulated" + escapedFileSeparator + "0" + escapedFileSeparator +".+$");
        boolean externalStorage4 = path.matches("^" + escapedFileSeparator + "storage" + escapedFileSeparator + "([A-Za-z0-9]){4}-([A-Za-z0-9]){4}" + escapedFileSeparator + ".+$");
        boolean externalStorage5 = path.matches("^" + escapedFileSeparator + "storage" + escapedFileSeparator + "sdcard" + escapedFileSeparator + ".+$");
        boolean internalStorage1 = path.matches("^" + escapedFileSeparator + "data" + escapedFileSeparator + "local" + escapedFileSeparator + "tmp" + escapedFileSeparator + ".+$");

        if (externalStorage1 || externalStorage2 || externalStorage3 || externalStorage4 || externalStorage5 || internalStorage1) {
            return;
        }

        throw new SecurityException("User shouldn't try to read files from the path: '" + uri.getPath() + "'");
    }

    /**
     * Helper method to read a file natively.
     *
     * @param uri The {@link Uri} which should point to a {@link File}.
     */
    private void readFile(@NonNull final Uri uri) {
        logger.info("readFile");
        final List<String> allowedPaths = List.of(UtilsContext.getSdCardPath(this), UtilsContext.getInternalStoragePath(this));
        final boolean isAllowedPath;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            final Path normalizedPath = FileSystems.getDefault().getPath(uri.getPath()).normalize();
            final String normalizedPathStr = UtilsContext.cleanStoragePath(normalizedPath.toFile().getAbsolutePath());
            isAllowedPath = allowedPaths.stream().anyMatch(normalizedPathStr::startsWith);
        } else {
            final String normalizedPath = Files.simplifyPath(new File(Objects.requireNonNull(uri.getPath())).getAbsolutePath());
            isAllowedPath = StreamSupport.stream(allowedPaths).anyMatch(normalizedPath::startsWith);
        }
        if (!isAllowedPath) {
            throw new SecurityException("The provided file path is not from a safe internal storage or external SD Card path.");
        }

        final String filePath = getPathFromFile(uri);
        logger.info("Will read the following file: '" + filePath + "'");

        try (ParcelFileDescriptor parcelFileDescriptor = Objects.requireNonNull(getContentResolver().openFileDescriptor(uri, "r"))) {
            logger.info("Opened AssetFileDescriptor");
            final int fileDescriptor = parcelFileDescriptor.getFd();
            final long fileSize = parcelFileDescriptor.getStatSize();

            logger.info("Will read the following file: '" + filePath + "', [fd: " + fileDescriptor + ", size: " + fileSize +  "]");
            // Important: Native layer shouldn't assume ownership of this fd and close it.
            readFile(fileDescriptor, fileSize, filePath);
        } catch (final Exception ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#readFile");
            throw new FailureException(ex);
        }
        logger.info("Path '" + filePath +"' already read.");
    }

    /**
     * Create a Ray Tracer {@link Config} from the selected {@link NumberPicker}s
     * in the Android UI.
     *
     * @param scenePath The path to the OBJ scene file.
     * @return A {@link Config}.
     */
    @NonNull
    private Config createConfigFromUI(@NonNull final String scenePath) {
        final Pair<Integer, Integer> resolution =
            Utils.getResolutionFromPicker(this.pickerResolutions);

        final Config.Builder builder = Config.Builder.Companion.create();
        builder.setScene(this.pickerScene.getValue());
        builder.setShader(this.pickerShader.getValue());
        builder.setAccelerator(this.pickerAccelerator.getValue());

        final ConfigSamples.Builder builderConfigSamples = ConfigSamples.Builder.Companion.create();
        builderConfigSamples.setSamplesPixel(Utils.getValueFromPicker(this.pickerSamplesPixel));
        builderConfigSamples.setSamplesLight(Utils.getValueFromPicker(this.pickerSamplesLight));
        builder.setConfigSamples(builderConfigSamples.build());
        final ConfigResolution.Builder builderConfigRes = ConfigResolution.Builder.Companion.create();
        builderConfigRes.setWidth(resolution.getFirst());
        builderConfigRes.setHeight(resolution.getSecond());
        builder.setConfigResolution(builderConfigRes.build());
        final int startOfExtension = scenePath.lastIndexOf('.');
        final String filePathWithoutExtension;
        if (startOfExtension >= 0) {
            filePathWithoutExtension = scenePath.substring(0, startOfExtension);
        } else {
            filePathWithoutExtension = scenePath;
        }
        builder.setObjFilePath(filePathWithoutExtension + ".obj");
        builder.setMatFilePath(filePathWithoutExtension + ".mtl");
        builder.setCamFilePath(filePathWithoutExtension + ".cam");
        builder.setThreads(this.pickerThreads.getValue());
        builder.setRasterize(this.checkBoxRasterize.isChecked());

        return builder.build();
    }

    /**
     * Helper method which calls a new {@link Activity} with a file manager to
     * select the OBJ file for the Ray Tracer engine to load.
     */
    private void callFileManager() {
        logger.info("callFileManager");
        final Intent intent = createIntentToLoadFiles(getPackageName());
        try {
            startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
        } catch (final ActivityNotFoundException ex) {
            UtilsLogging.logThrowable(ex, "MainActivity#callFileManager");
            Toast.makeText(this, ConstantsToast.PLEASE_INSTALL_FILE_MANAGER, Toast.LENGTH_LONG)
                .show();
        }

        final String message = "callFileManager" + ConstantsMethods.FINISHED;
        logger.info(message);
    }

    /**
     * Helper method that initializes all the pickers fields of this
     * {@link Activity}.
     *
     * @param bundle The data state of the {@link Activity}.
     */
    private void initializePickers(final Optional<Bundle> bundle) {
        initializePicker(this.pickerScene, bundle.map(x -> x.getInt(ConstantsUI.PICKER_SCENE))
            .orElse(0), Scene.getNames());
        initializePicker(this.pickerShader, bundle.map(x -> x.getInt(ConstantsUI.PICKER_SHADER))
            .orElse(0), Shader.getNames());
        initializePicker(this.pickerAccelerator,
            bundle.map(x -> x.getInt(ConstantsUI.PICKER_ACCELERATOR))
                .orElse(1), Accelerator.getNames());

        final String[] samplesPixel = IntStreams.range(0, 99)
            .map(value -> (value + 1) * (value + 1))
            .mapToObj(String::valueOf)
            .toArray(String[]::new);
        initializePicker(this.pickerSamplesPixel,
            bundle.map(x -> x.getInt(ConstantsUI.PICKER_SAMPLES_PIXEL))
                .orElse(1), samplesPixel);

        final String[] samplesLight = IntStreams.range(0, 100)
            .map(value -> value + 1)
            .mapToObj(String::valueOf)
            .toArray(String[]::new);
        initializePicker(this.pickerSamplesLight,
            bundle.map(x -> x.getInt(ConstantsUI.PICKER_SAMPLES_LIGHT))
                .orElse(1), samplesLight);

        initializePickerThreads(bundle.map(x -> x.getInt(ConstantsUI.PICKER_THREADS))
            .orElse(1));

        initializePickerResolutions(bundle.map(x -> x.getInt(ConstantsUI.PICKER_SIZE))
            .orElse(4));
    }

    /**
     * Helper method that sets up the {@link GLSurfaceView.Renderer} in the
     * {@link DrawView}.
     * This method loads the GLSL shaders and pass them to the
     * {@link GLSurfaceView.Renderer}.
     *
     * @param textView     The {@link TextView} for the {@link GLSurfaceView.Renderer} to pass to
     *                     the {@link RenderTask} when the Ray Tracing process starts.
     * @param renderButton The {@link TextView} for the {@link GLSurfaceView.Renderer} to
     *                     pass to the {@link RenderTask} when the Ray Tracing
     *                     process starts.
     */
    private void setupRenderer(final TextView textView, final Button renderButton) {
        logger.info("setupRenderer start");

        this.drawView.setVisibility(View.INVISIBLE);
        this.drawView.setEGLContextClientVersion(MyEglContextFactory.EGL_CONTEXT_CLIENT_VERSION);
        this.drawView.setEGLConfigChooser(8, 8, 8, 8, 3 * 8, 0);

        final ActivityManager activityManager = (ActivityManager) getSystemService(
            Context.ACTIVITY_SERVICE);
        this.drawView.setViewAndActivityManager(textView, activityManager);
        this.drawView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

        renderButton.setOnLongClickListener((final View view) -> {
            onDestroy(); // Necessary to stop any previous render before recreating the Activity.
            recreate();
            return false;
        });

        final String shadersPath = ConstantsUI.PATH_SHADERS + ConstantsUI.FILE_SEPARATOR;
        final ImmutableMap<Integer, String> shadersPaths = ImmutableMap.of(
            GLES20.GL_VERTEX_SHADER, shadersPath + "VertexShader.glsl",
            GLES20.GL_FRAGMENT_SHADER, shadersPath + "FragmentShader.glsl");
        final Map<Integer, String> shadersRayTracing =
            UtilsContext.readShaders(this, shadersPaths);

        final ImmutableMap<Integer, String> shadersPreviewPaths = ImmutableMap.of(
            GLES20.GL_VERTEX_SHADER, shadersPath + "VertexShaderRaster.glsl",
            GLES20.GL_FRAGMENT_SHADER, shadersPath + "FragmentShaderRaster.glsl");
        final Map<Integer, String> shadersPreview =
            UtilsContext.readShaders(this, shadersPreviewPaths);

        this.drawView.setUpShadersCode(shadersRayTracing, shadersPreview);
        this.drawView.setUpButtonRender(renderButton);
        this.drawView.setVisibility(View.VISIBLE);
        this.drawView.setPreserveEGLContextOnPause(true);

        logger.info("setupRenderer finish");
    }

    /**
     * Initializes the {@link #checkBoxRasterize} field.
     *
     * @param checkBoxRasterize The default value to put in the
     *                          {@link #checkBoxRasterize} field.
     */
    private void initializeCheckBoxRasterize(final boolean checkBoxRasterize) {
        this.checkBoxRasterize.setChecked(checkBoxRasterize);
        final int scale = Math.round(getResources().getDisplayMetrics().density);
        this.checkBoxRasterize.setPadding(
            this.checkBoxRasterize.getPaddingLeft() - (5 * scale),
            this.checkBoxRasterize.getPaddingTop(),
            this.checkBoxRasterize.getPaddingRight(),
            this.checkBoxRasterize.getPaddingBottom()
        );
    }

    /**
     * Initializes the {@link #pickerResolutions} field.
     *
     * @param pickerSizes The default value to put in the
     *                    {@link #pickerResolutions} field.
     */
    private void initializePickerResolutions(final int pickerSizes) {
        logger.info("initializePickerResolutions start");
        final int maxSizes = 9;
        this.pickerResolutions.setMinValue(1);
        this.pickerResolutions.setMaxValue(maxSizes - 1);
        this.pickerResolutions.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        this.pickerResolutions.setValue(pickerSizes);

        // TODO: For Android API < 15, this method crashes, so it's necessary to investigate it.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            this.pickerResolutions.setWrapSelectorWheel(true);
        }

        final ViewTreeObserver vto = this.drawView.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(() -> {
            logger.info("initializePickerResolutions 1");
            final double widthView = this.drawView.getWidth();
            final double heightView = this.drawView.getHeight();

            final String[] resolutions = IntStreams.rangeClosed(2, maxSizes)
                // double.class::cast method throws `ClassCastException: Integer cannot be cast to double`
                .mapToDouble(Double::valueOf)
                .map(value -> (value + 1.0) * 0.1)
                .map(value -> value * value)
                .mapToObj(value -> {
                    resetErrno(); // Necessary to avoid 'EWOULDBLOCK'.
                    final int width = rtResize((int) Math.round(widthView * value));
                    final int height = rtResize((int) Math.round(heightView * value));
                    return String.valueOf(width) + 'x' + height;
                })
                .toArray(String[]::new);

            this.pickerResolutions.setDisplayedValues(resolutions);
            logger.info("initializePickerResolutions 2");
        });
        logger.info("initializePickerResolutions finish");
    }

    /**
     * Initializes the {@link #pickerThreads} field.
     *
     * @param pickerThreads The default value to put in the
     *                      {@link #pickerThreads} field.
     */
    private void initializePickerThreads(final int pickerThreads) {
        final int maxCores = UtilsContext.getNumOfCores(this);
        this.pickerThreads.setMinValue(1);
        this.pickerThreads.setMaxValue(maxCores);
        this.pickerThreads.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        this.pickerThreads.setValue(pickerThreads);

        // TODO: For Android API < 15, this method crashes, so it's necessary to investigate it.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            this.pickerThreads.setWrapSelectorWheel(true);
        }
    }

    /**
     * Helper method that initializes the fields that are {@link View}s.
     */
    private void initializeViews() {
        this.drawView = findViewById(R.id.drawLayout);
        this.pickerScene = findViewById(R.id.pickerScene);
        this.pickerShader = findViewById(R.id.pickerShader);
        this.pickerSamplesPixel = findViewById(R.id.pickerSamplesPixel);
        this.pickerSamplesLight = findViewById(R.id.pickerSamplesLight);
        this.pickerAccelerator = findViewById(R.id.pickerAccelerator);
        this.pickerThreads = findViewById(R.id.pickerThreads);
        this.pickerResolutions = findViewById(R.id.pickerSize);
        this.checkBoxRasterize = findViewById(R.id.preview);
        validateViews();
    }

    /**
     * Helper method that validates the fields that are {@link View}s.
     */
    private void validateViews() {
        Preconditions.checkNotNull(this.pickerResolutions, "pickerResolutions shouldn't be null");
        Preconditions.checkNotNull(this.pickerThreads, "pickerThreads shouldn't be null");
        Preconditions.checkNotNull(this.pickerAccelerator, "pickerAccelerator shouldn't be null");
        Preconditions.checkNotNull(this.pickerSamplesLight, "pickerSamplesLight shouldn't be null");
        Preconditions.checkNotNull(this.pickerSamplesPixel, "pickerSamplesPixel shouldn't be null");
        Preconditions.checkNotNull(this.pickerShader, "pickerShader shouldn't be null");
        Preconditions.checkNotNull(this.pickerScene, "pickerScene shouldn't be null");
        Preconditions.checkNotNull(this.drawView, "drawView shouldn't be null");
    }

    /**
     * Sets the {@link #currentInstance}.
     */
    private void setCurrentInstance() {
        currentInstance = this;
    }

}