io.flutter.preview.RenderHelper.java Source code

Java tutorial

Introduction

Here is the source code for io.flutter.preview.RenderHelper.java

Source

/*
 * Copyright 2018 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
package io.flutter.preview;

import com.google.common.io.Resources;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.gson.JsonObject;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService;
import io.flutter.pub.PubRoot;
import io.flutter.run.daemon.DaemonApi;
import io.flutter.run.daemon.FlutterApp;
import io.flutter.run.daemon.FlutterDevice;
import io.flutter.run.daemon.RunMode;
import io.flutter.sdk.FlutterCommand;
import io.flutter.sdk.FlutterSdk;
import org.dartlang.analysis.server.protocol.FlutterOutline;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class RenderHelper {
    private final Project myProject;
    private final Listener myListener;

    private final FlutterSdk myFlutterSdk;
    private final RenderThread myRenderThread = new RenderThread();

    private PubRoot myPubRoot;
    private Module myModule;

    private VirtualFile myFile;
    private FlutterOutline myFileOutline;
    private String myInstrumentedCode;

    private FlutterOutline myWidgetOutline;

    private int myWidth = 0;
    private int myHeight = 0;

    public RenderHelper(@NotNull Project project, @NotNull Listener listener) {
        myProject = project;
        myListener = listener;

        myFlutterSdk = FlutterSdk.getFlutterSdk(myProject);

        myRenderThread.start();
    }

    /**
     * Set a new file, with or without outline.
     */
    public void setFile(@Nullable VirtualFile file, @Nullable FlutterOutline fileOutline,
            @Nullable String instrumentedCode) {
        myFile = file;
        myFileOutline = fileOutline;
        myInstrumentedCode = instrumentedCode;

        myPubRoot = PubRoot.forFile(file);
        if (myPubRoot != null) {
            myModule = myPubRoot.getModule(myProject);
        }

        myWidgetOutline = null;
    }

    /**
     * Set the size and render the current widget with this size.
     */
    public void setSize(int width, int height) {
        myWidth = width;
        myHeight = height;
        scheduleRendering();
    }

    /**
     * Set the offset in the current file, and render if a new widget.
     */
    public void setOffset(int offset) {
        final FlutterOutline previousWidgetOutline = myWidgetOutline;
        myWidgetOutline = getContainingWidgetOutline(offset);
        if (myWidgetOutline == null) {
            myRenderThread.setRequest(null);
            myListener.onFailure(RenderProblemKind.NO_WIDGET, null);
        } else if (myWidgetOutline.getRenderConstructor() == null) {
            myRenderThread.setRequest(null);
            myListener.onFailure(RenderProblemKind.NOT_RENDERABLE_WIDGET, myWidgetOutline);
            myWidgetOutline = null;
        } else if (myWidgetOutline != previousWidgetOutline) {
            myListener.onRenderableWidget(myWidgetOutline);
            scheduleRendering();
        }
    }

    /**
     * Return the outline for the widget class that is associated with the given offset.
     * <p>
     * Return null if there is no associated widget class outline.
     */
    private FlutterOutline getContainingWidgetOutline(int offset) {
        if (myFileOutline != null && myFileOutline.getChildren() != null) {
            for (FlutterOutline outline : myFileOutline.getChildren()) {
                final int outlineStart = getConvertedFileOffset(outline.getOffset());
                final int outlineEnd = getConvertedFileOffset(outline.getOffset() + outline.getLength());

                int stateStart = -1;
                int stateEnd = -1;
                if (outline.getStateOffset() != null && outline.getStateLength() != null) {
                    stateStart = getConvertedFileOffset(outline.getStateOffset());
                    stateEnd = getConvertedFileOffset(outline.getStateOffset() + outline.getStateLength());
                }

                if (outlineStart < offset && offset < outlineEnd || stateStart < offset && offset < stateEnd) {
                    if (outline.isWidgetClass()) {
                        return outline;
                    } else {
                        return null;
                    }
                }
            }
        }
        return null;
    }

    private int getConvertedFileOffset(int offset) {
        return DartAnalysisServerService.getInstance(myProject).getConvertedOffset(myFile, offset);
    }

    private void scheduleRendering() {
        if (myPubRoot == null || myModule == null || myInstrumentedCode == null || myWidgetOutline == null
                || myWidth == 0 || myHeight == 0) {
            return;
        }

        // TODO: Expose this as an error to the user.
        if (myFlutterSdk == null) {
            return;
        }

        final String widgetClass = myWidgetOutline.getDartElement().getName();
        final String constructor = myWidgetOutline.getRenderConstructor();
        final RenderRequest request = new RenderRequest(myFlutterSdk, myProject, myPubRoot, myModule,
                myInstrumentedCode, myWidgetOutline, widgetClass, constructor, myWidth, myHeight, myListener);
        myRenderThread.setRequest(request);

        myListener.onSchedule(myWidgetOutline);
    }

    public interface Listener {
        void onSchedule(@NotNull FlutterOutline widget);

        void onResponse(@NotNull FlutterOutline widget, @NotNull JsonObject response);

        void onFailure(@NotNull RenderProblemKind kind, @Nullable FlutterOutline widget);

        void onRenderableWidget(@NotNull FlutterOutline widget);

        void onLocalException(@NotNull FlutterOutline widget, @NotNull Throwable localException);

        void onRemoteException(@NotNull FlutterOutline widget, @NotNull JsonObject remoteException);
    }
}

/**
 * Container with a set of parameters for rendering.
 */
class RenderRequest {
    static int nextId = 0;

    final int id = ++nextId;

    /**
     * Whether this request is still active, or another one has been scheduled.
     */
    boolean isActive = true;

    @NotNull
    final FlutterSdk flutterSdk;

    @NotNull
    final Project project;
    @NotNull
    final PubRoot pubRoot;
    @NotNull
    final Module module;

    @NotNull
    final String codeToRender;
    @NotNull
    final FlutterOutline widget;
    @NotNull
    final String widgetClass;
    @NotNull
    final String widgetConstructor;

    final int width;
    final int height;

    final RenderHelper.Listener listener;

    RenderRequest(@NotNull FlutterSdk flutterSdk, @NotNull Project project, @NotNull PubRoot pubRoot,
            @NotNull Module module, @NotNull String codeToRender, @NotNull FlutterOutline widget,
            @NotNull String widgetClass, @NotNull String widgetConstructor, int width, int height,
            RenderHelper.Listener listener) {
        this.flutterSdk = flutterSdk;
        this.project = project;
        this.pubRoot = pubRoot;
        this.module = module;
        this.codeToRender = codeToRender;
        this.widget = widget;
        this.widgetClass = widgetClass;
        this.widgetConstructor = widgetConstructor;
        this.width = width;
        this.height = height;
        this.listener = listener;
    }
}

class RenderThread extends Thread {
    final Object myRequestLock = new Object();
    RenderRequest myLastRequest;
    RenderRequest myNextRequest;

    RenderRequest myProcessRequest;
    FlutterApp myApp;

    RenderThread() {
        setDaemon(true);
    }

    void setRequest(@Nullable RenderRequest request) {
        synchronized (myRequestLock) {
            if (myLastRequest != null) {
                myLastRequest.isActive = false;
                myLastRequest = null;
            }
            myNextRequest = request;
            myRequestLock.notifyAll();
        }
    }

    @Override
    public void run() {
        //noinspection InfiniteLoopStatement
        while (true) {
            final RenderRequest request;
            try {
                synchronized (myRequestLock) {
                    if (myNextRequest == null) {
                        myRequestLock.wait();
                    }

                    myLastRequest = myNextRequest;

                    request = myNextRequest;
                    myNextRequest = null;
                    if (request == null) {
                        continue;
                    }
                }
            } catch (InterruptedException ignored) {
                continue;
            }

            render(request);
        }
    }

    private void render(@NotNull RenderRequest request) {
        final FlutterOutline widget = request.widget;

        try {
            final String packagePath = request.pubRoot.getPath();
            final File dartToolDirectory = new File(packagePath, ".dart_tool");
            final File flutterDirectory = new File(dartToolDirectory, "flutter");
            final File designerDirectory = new File(flutterDirectory, "designer");

            final File toRenderFile = new File(designerDirectory, "to_render.dart");
            FileUtil.writeToFile(toRenderFile, request.codeToRender);

            final String widgetCreation = "new " + request.widgetClass + "." + request.widgetConstructor + "();";

            final URL templateUri = RenderHelper.class.getResource("render_server_template.txt");
            String template = Resources.toString(templateUri, StandardCharsets.UTF_8);
            template = template.replace("// TEMPLATE_VALUE: import library to render",
                    "import '" + toRenderFile.getName() + "';");
            template = template.replace("new Container(); // TEMPLATE_VALUE: create widget", widgetCreation);
            template = template.replace("{}; // TEMPLATE_VALUE: use flutterDesignerWidgets",
                    "flutterDesignerWidgets;");
            template = template.replace("350.0 /*TEMPLATE_VALUE: width*/", request.width + ".0");
            template = template.replace("400.0 /*TEMPLATE_VALUE: height*/", request.height + ".0");

            final File renderServerFile = new File(designerDirectory, "render_server.dart");
            final String renderServerPath = renderServerFile.getPath();
            FileUtil.writeToFile(renderServerFile, template);

            // If the process is dead, clear the instance.
            if (myApp != null && !myApp.isConnected()) {
                myProcessRequest = null;
                myApp = null;
            }

            // Check if the current render server process is compatible with the new request.
            // If it is, attempt to perform hot reload.
            // If not successful, terminate the process.
            boolean canRenderWithCurrentProcess = false;
            if (myProcessRequest != null && myApp != null) {
                if (Objects.equals(myProcessRequest.pubRoot.getPath(), packagePath)) {
                    try {
                        final DaemonApi.RestartResult restartResult = myApp.performHotReload(false).get(5000,
                                TimeUnit.MILLISECONDS);
                        if (restartResult.ok()) {
                            canRenderWithCurrentProcess = true;
                        }
                    } catch (Throwable ignored) {
                    }
                }

                if (!canRenderWithCurrentProcess) {
                    terminateCurrentProcess("Project root or file changed, or reload failed");
                }
            }

            // If there is no rendering server process, start a new one.
            // Wait for it to start.
            if (myApp == null) {
                final FlutterCommand command = request.flutterSdk.flutterRunOnTester(request.pubRoot,
                        renderServerPath);
                final GeneralCommandLine commandLine = command.createGeneralCommandLine(request.project);

                // Windows is not a supported Flutter target platform.
                // Set FLUTTER_TEST to force using Android (everywhere, not just on Windows)
                commandLine.getEnvironment().put("FLUTTER_TEST", "true");

                final FlutterApp app = FlutterApp.start(new ExecutionEnvironment(), request.project, request.module,
                        RunMode.DEBUG, FlutterDevice.getTester(), commandLine, null, null);

                final CountDownLatch startedLatch = new CountDownLatch(1);
                app.addStateListener(new FlutterApp.FlutterAppListener() {
                    @Override
                    public void stateChanged(FlutterApp.State newState) {
                        if (newState == FlutterApp.State.STARTED) {
                            startedLatch.countDown();
                        }
                        if (newState == FlutterApp.State.TERMINATING) {
                            myProcessRequest = null;
                            myApp = null;
                        }
                    }
                });

                final boolean started = Uninterruptibles.awaitUninterruptibly(startedLatch, 10000,
                        TimeUnit.MILLISECONDS);
                if (!started) {
                    terminateCurrentProcess("Initial start timeout.");
                    invokeIfSameRequest(request,
                            () -> request.listener.onFailure(RenderProblemKind.TIMEOUT_START, request.widget));
                    return;
                }

                myProcessRequest = request;
                myApp = app;
            }

            // Ask to render the widget.
            final CountDownLatch responseReceivedLatch = new CountDownLatch(1);
            final AtomicReference<JsonObject> responseRef = new AtomicReference<>();
            myApp.callServiceExtension("ext.flutter.designer.render").thenAccept((response) -> {
                responseRef.set(response);
                responseReceivedLatch.countDown();
            });

            // Wait for the response.
            final boolean responseReceived = Uninterruptibles.awaitUninterruptibly(responseReceivedLatch, 4000,
                    TimeUnit.MILLISECONDS);
            if (!responseReceived) {
                terminateCurrentProcess("Render response timeout.");
                invokeIfSameRequest(request,
                        () -> request.listener.onFailure(RenderProblemKind.TIMEOUT_RENDER, widget));
                return;
            }
            final JsonObject response = responseRef.get();

            if (response.has("exception")) {
                invokeIfSameRequest(request, () -> request.listener.onRemoteException(widget, response));
                return;
            }

            // Send the respose to the client.
            invokeIfSameRequest(request, () -> request.listener.onResponse(widget, response));
        } catch (Throwable e) {
            terminateCurrentProcess("Exception");
            invokeIfSameRequest(request, () -> request.listener.onLocalException(widget, e));
        }
    }

    private void invokeIfSameRequest(RenderRequest request, Runnable runnable) {
        if (request.isActive) {
            runnable.run();
        }
    }

    private void terminateCurrentProcess(String reason) {
        myProcessRequest = null;

        if (myApp != null) {
            myApp.getProcessHandler().destroyProcess();
            myApp = null;
        }
    }
}