app/src/main/java/puscas/mobilertapp/DrawView.java
package puscas.mobilertapp;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.common.util.concurrent.Uninterruptibles;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java8.util.Optional;
import kotlin.Pair;
import puscas.mobilertapp.configs.Config;
import puscas.mobilertapp.configs.ConfigResolution;
import puscas.mobilertapp.constants.ConstantsError;
import puscas.mobilertapp.constants.ConstantsMethods;
import puscas.mobilertapp.constants.ConstantsRenderer;
import puscas.mobilertapp.constants.ConstantsToast;
import puscas.mobilertapp.constants.State;
import puscas.mobilertapp.exceptions.FailureException;
import puscas.mobilertapp.exceptions.LowMemoryException;
import puscas.mobilertapp.utils.Utils;
import puscas.mobilertapp.utils.UtilsLogging;
/**
* The {@link GLSurfaceView} to show the scene being rendered.
*/
public final class DrawView extends GLSurfaceView {
/**
* Logger for this class.
*/
private static final Logger logger = Logger.getLogger(DrawView.class.getSimpleName());
/**
* The {@link GLSurfaceView.Renderer}.
*/
private final MainRenderer renderer = new MainRenderer();
/**
* The {@link ExecutorService} which holds
* {@link ConstantsRenderer#NUMBER_THREADS} number of threads that will
* create Ray Tracer engine renderer.
*/
private final ExecutorService executorService = Executors.newFixedThreadPool(ConstantsRenderer.NUMBER_THREADS);
/**
* The changing configs.
*
* @see Activity#isChangingConfigurations()
*/
private boolean changingConfigs = false;
/**
* The last task submitted to {@link #executorService}.
*/
private Pair<Long, Future<Boolean>> lastTask = null;
/**
* The constructor for this class.
*
* @param context The context of the Android system.
*/
public DrawView(@NonNull final Context context) {
super(context);
logger.info("DrawView start 1");
this.renderer.prepareRenderer(this::requestRender);
initEglContextFactory();
logger.info("DrawView finished 1");
}
/**
* The constructor for this class.
*
* @param context The context of the Android system.
* @param attrs The attributes of the Android system.
*/
public DrawView(@NonNull final Context context,
@NonNull final AttributeSet attrs) {
super(context, attrs);
logger.info("DrawView start 2");
this.renderer.prepareRenderer(this::requestRender);
initEglContextFactory();
logger.info("DrawView finished 2");
}
/**
* Gets the {@link #changingConfigs}.
*
* @return The {@link #changingConfigs}.
*/
public boolean isChangingConfigs() {
return changingConfigs;
}
/**
* Gets the {@link #renderer}.
*
* @return The {@link GLSurfaceView.Renderer}.
*/
@VisibleForTesting
public MainRenderer getRenderer() {
return renderer;
}
/**
* Helper method which initiates the {@link GLSurfaceView.EGLContextFactory}.
*/
private void initEglContextFactory() {
logger.info("initEglContextFactory start");
this.changingConfigs = false;
final GLSurfaceView.EGLContextFactory eglContextFactory = new MyEglContextFactory(this);
setEGLContextClientVersion(MyEglContextFactory.EGL_CONTEXT_CLIENT_VERSION);
setEGLContextFactory(eglContextFactory);
logger.info("initEglContextFactory finished");
}
/**
* Stops the Ray Tracer engine and sets its {@link State} to {@link State#STOPPED}.
*
* @param wait Whether it should wait for the Ray Tracer engine to stop.
*/
@VisibleForTesting
native void rtStopRender(boolean wait);
/**
* Sets the Ray Tracer engine {@link State} to {@link State#BUSY}.
*
* @param wait Whether it should wait for the Ray Tracer engine to stop at the beginning.
* @implNote Necessary to be package visible so it's possible to mock it in the unit tests,
* otherwise an {@link UnsatisfiedLinkError} is thrown.
*/
@VisibleForTesting
native void rtStartRender(boolean wait);
/**
* Gets the number of lights in the scene.
*
* @return The number of lights.
*/
@VisibleForTesting
native int rtGetNumberOfLights();
/**
* Helper method which gets the instance of the {@link Activity}.
*
* @return The current {@link Activity}.
*/
@NonNull
@VisibleForTesting
Activity getActivity() {
Context context = getContext();
while (!(context instanceof Activity) && context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
}
if (context instanceof Activity) {
return (Activity) context;
}
throw new IllegalStateException(ConstantsError.UNABLE_TO_FIND_AN_ACTIVITY + context);
}
/**
* Sets the {@link DrawView#renderer} as the {@link GLSurfaceView.Renderer}
* of this object.
*
* @param textView The {@link TextView} to set in the
* {@link DrawView#renderer}.
* @param activityManager The {@link ActivityManager} to set in the
* {@link DrawView#renderer}.
*/
void setViewAndActivityManager(final TextView textView,
final ActivityManager activityManager) {
this.renderer.setTextView(textView);
this.renderer.setActivityManager(activityManager);
try {
this.renderer.checksFreeMemory(1, () -> {});
} catch (final Exception ex) {
UtilsLogging.logThrowable(ex, "DrawView#setViewAndActivityManager");
throw new FailureException(ex);
}
setRenderer(this.renderer);
}
/**
* Stops the Ray Tracer engine and waits for it to stop rendering.
*/
void stopDrawing() {
logger.info("stopDrawing");
rtStopRender(true);
Optional.ofNullable(this.lastTask)
.map(Pair::getSecond)
.ifPresent(task -> task.cancel(false));
waitLastTask();
this.renderer.updateButton(R.string.render);
finishRenderer();
final String message = "stopDrawing" + ConstantsMethods.FINISHED;
logger.info(message);
}
/**
* Asynchronously creates the requested scene and starts rendering it.
*
* @param config The ray tracer configuration.
*/
synchronized void renderScene(@NonNull final Config config) {
logger.info(ConstantsMethods.RENDER_SCENE);
MainActivity.resetErrno();
stopDrawing();
waitLastTask();
rtStartRender(false);
final long tasksAlreadyDone = this.executorService instanceof ThreadPoolExecutor? ((ThreadPoolExecutor) this.executorService).getCompletedTaskCount() : -1L;
final Future<Boolean> newTask = this.executorService.submit(() -> {
this.renderer.waitLastTask();
try {
rtStartRender(true);
startRayTracing(config);
return Boolean.TRUE;
} catch (final LowMemoryException ex) {
UtilsLogging.logThrowable(ex, "DrawView#renderScene");
MainActivity.showUiMessage(ConstantsToast.DEVICE_WITHOUT_ENOUGH_MEMORY + ex.getMessage());
} catch (final Exception ex) {
UtilsLogging.logThrowable(ex, "DrawView#renderScene");
renderer.resetStats();
MainActivity.showUiMessage(ConstantsToast.COULD_NOT_LOAD_THE_SCENE + ex.getMessage());
}
final String messageFailed = ConstantsMethods.RENDER_SCENE + " executor failed";
logger.severe(messageFailed);
this.renderer.rtFinishRender();
// Only the UI thread can update the text in the Render button.
post(() -> this.renderer.updateButton(R.string.render));
return Boolean.FALSE;
});
this.lastTask = new Pair<>(tasksAlreadyDone, newTask);
// This should be executed by the UI thread, so it's good to go.
this.renderer.updateButton(R.string.stop);
final String messageFinished = ConstantsMethods.RENDER_SCENE + ConstantsMethods.FINISHED;
logger.info(messageFinished);
}
/**
* Helper method that prepares the scene and starts the Ray Tracing engine
* to render it.
*
* @param config The ray tracer configuration.
* @throws LowMemoryException If the device has low free memory.
*/
@VisibleForTesting
void startRayTracing(@NonNull final Config config) throws LowMemoryException {
final String message = "startRayTracing executor";
logger.info(message);
createScene(config);
requestRender(); // This will make the `MainRenderer#onDrawFrame` method to be called.
final String messageFinished = "startRayTracing executor" + ConstantsMethods.FINISHED;
logger.info(messageFinished);
}
/**
* Waits for the result of the last task submitted to the {@link ExecutorService}.
*/
void waitLastTask() {
logger.info("waitLastTask");
while (this.lastTask != null && ((ThreadPoolExecutor) this.executorService).getCompletedTaskCount() <= this.lastTask.getFirst()) {
logger.info("Waiting for last task to finish create the renderer.");
Uninterruptibles.sleepUninterruptibly(500L, TimeUnit.MILLISECONDS);
}
this.renderer.waitLastTask();
Optional.ofNullable(this.lastTask)
.ifPresent(task -> {
try {
task.getSecond().get(1L, TimeUnit.DAYS);
} catch (final ExecutionException | TimeoutException | RuntimeException ex) {
UtilsLogging.logThrowable(ex, "DrawView#waitLastTask");
} catch (final InterruptedException ex) {
UtilsLogging.logThrowable(ex, "DrawView#waitLastTask");
Thread.currentThread().interrupt();
} finally {
Utils.handleInterruption("DrawView#waitLastTask");
}
});
final String message = "waitLastTask" + ConstantsMethods.FINISHED;
logger.info(message);
}
/**
* Loads the scene and creates the Ray Tracer renderer.
*
* @param config The ray tracer configuration.
* @throws LowMemoryException If the device has low free memory.
*/
@VisibleForTesting
void createScene(final Config config) throws LowMemoryException {
logger.info("createScene");
MainActivity.resetErrno();
final int numPrimitives = this.renderer.rtInitialize(config);
if (numPrimitives <= -1) {
throw new FailureException("Couldn't load the scene.");
}
this.renderer.resetStats(config.getThreads(), config.getConfigSamples(),
numPrimitives, rtGetNumberOfLights());
final int widthView = getWidth();
final int heightView = getHeight();
final ConfigResolution.Builder builder = ConfigResolution.Builder.Companion.create();
builder.setWidth(widthView);
builder.setHeight(heightView);
queueEvent(() -> this.renderer.setBitmap(
config.getConfigResolution(),
builder.build(),
config.getRasterize()
));
}
@Override
public void onPause() {
logger.info("onPause");
super.onPause();
logger.info("onPause");
final Activity activity = getActivity();
this.changingConfigs = activity.isChangingConfigurations();
MainActivity.resetErrno();
stopDrawing();
setVisibility(View.GONE);
final String message = "onPause" + ConstantsMethods.FINISHED;
logger.info(message);
}
@Override
protected void onDetachedFromWindow() {
logger.info(ConstantsMethods.ON_DETACHED_FROM_WINDOW);
super.onDetachedFromWindow();
stopDrawing();
finishRenderer();
// We need to call `closeRenderer` method with the GL rendering thread.
queueEvent(this.renderer::closeRenderer);
setVisibility(View.GONE);
final String message = ConstantsMethods.ON_DETACHED_FROM_WINDOW + ConstantsMethods.FINISHED;
logger.info(message);
}
@Override
public void onWindowFocusChanged(final boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
logger.info("onWindowFocusChanged hasWindowFocus: " + hasWindowFocus + ", visibility: " + getVisibility());
if (hasWindowFocus && getVisibility() == View.GONE) {
logger.info("Setting window to: visible");
setVisibility(View.VISIBLE);
}
final String message = "onWindowFocusChanged" + ConstantsMethods.FINISHED;
logger.info(message);
}
/**
* This is an auxiliary method that serves as a middle man to let outside
* classes like {@link MainActivity} terminate properly the Ray Tracing
* engine without having to not obey the law of Demeter.
*
* @implNote This method calls {@link MainRenderer#rtFinishRender()} and
* also {@link MainRenderer#freeArrays()}.
* @see <a href="https://en.wikipedia.org/wiki/Law_of_Demeter">Law of Demeter</a>
*/
void finishRenderer() {
logger.info("finishRenderer");
MainActivity.resetErrno();
this.renderer.rtFinishRender();
this.renderer.freeArrays();
}
/**
* This is an auxiliary method that serves as a middle man to let outside
* classes like {@link MainActivity} get the current {@link State} of the
* Ray Tracer engine without having to not obey the law of Demeter.
*
* @return The current Ray Tracer engine {@link State}.
*/
State getRayTracerState() {
logger.info("getRayTracerState");
MainActivity.resetErrno();
return this.renderer.getState();
}
/**
* Prepares the {@link MainRenderer} with the OpenGL shaders' code.
*
* @param shadersCode The shaders' code for the Ray Tracing engine.
* @param shadersPreviewCode The shaders' code for the OpenGL preview feature.
*/
void setUpShadersCode(final Map<Integer, String> shadersCode,
final Map<Integer, String> shadersPreviewCode) {
this.renderer.setUpShadersCode(shadersCode, shadersPreviewCode);
}
/**
* Prepares the {@link MainRenderer} with the render button for the {@link RenderTask}.
*
* @param button The render {@link Button}.
*/
void setUpButtonRender(final Button button) {
this.renderer.setButtonRender(button);
}
}