openblocks.yacodeblocks.DeviceReplCommController.java Source code

Java tutorial

Introduction

Here is the source code for openblocks.yacodeblocks.DeviceReplCommController.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt

package openblocks.yacodeblocks;

import com.google.appinventor.components.common.YaVersion;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Random;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JDialog;

import org.json.*;
import org.apache.commons.io.*;

import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;

/**
 * Class for communication with a Phone/Emulator containing an embedded Yail REPL.
 * Handles the lower levels of communication with a device.
 *
 * @author markf@google.com (Mark Friedman)
 */

public class DeviceReplCommController implements AndroidController.DeviceConnectionListener {

    public interface PostProcessor {
        void postProcess(String message);

        void onFailure(Throwable e);

        void onDisconnect(String serialNumber);

        void onConnect(String serialNumber);
    }

    private static final boolean DEBUG = true;

    private String host;
    private int port;
    private Socket socket;
    private OutputStream out;
    private InputStream in;
    private ReadWriteThread inputThread;
    private AndroidController androidController;
    private PostProcessor postProcessor;
    private PhoneCommManager phoneManager;

    private volatile boolean connectionHappy = false; // is there a device connected and working
    private volatile int devicesPluggedIn = 0;
    private volatile boolean stopWireless = false; // set to true to break out of the rendezvous server read loop prematurely

    private final String rendezvousUrl = "http://rendezvous.appinventor.mit.edu/rendezvous/"; // This should be parameterized (will be!)

    public DeviceReplCommController(String host, int port, AndroidController androidController,
            PostProcessor postProcessor) {
        this.host = host;
        this.port = port;
        this.androidController = androidController;
        this.postProcessor = postProcessor;
    }

    private void doWrite(String message) throws IOException {
        out.write(message.getBytes("UTF-8"));
        out.write('\n');
        if (DEBUG) {
            System.out.println(String.format("Sent '%s\n", message));
        }
    }

    /**
     * Send initial string to the REPL controller.  If the REPL controller
     * isn't actually connected, will take a series of escalating steps to
     * reconnect it.
     * @param message the string to send to the REPL controller
     * @param mustRestartApp if true, will restart the app before doing the send
     * @throws IOException, ExternalStorageException
     */
    public void sendInitial(String message, boolean mustRestartApp) throws IOException, ExternalStorageException {
        String selectedDevice = androidController.getSelectedDevice();
        if (DEBUG) {
            System.out.println("ReplController sending " + message);
            System.out.println("**** devicesPluggedIn: " + devicesPluggedIn + " selectedDevice: " + selectedDevice
                    + " connectionHappy: " + connectionHappy);
        }
        // TODO(kerr): deal with threading with respect to devices plugged in?

        // if think is connected, try a write.
        // if that fails, start debugging.

        String failedMessage = "Failed to restart communication for unknown reason.";
        if (connectionHappy && !mustRestartApp) {
            // we _think_ that it is connected...
            try {
                doWrite(message);
                connectionHappy = true;
                return;
            } catch (IOException e) {
                if (DEBUG) {
                    System.out.println("**** Exception while writing: " + e.getMessage());
                    System.out.println("     ... ignoring and trying to reconnect.");
                }
            }
        }
        connectionHappy = false;
        if (selectedDevice != null) {
            // Only check if there is a selected device

            /*
             *  Always reinstall app when there is a problem. The new "tight security"
             *  app will only ever accept one socket connection and disables itself
             *  as soon as it starts, so it cannot be restarted.
             */
            boolean restarted = false; // If true, failure was probably at the socket level
            try {
                if (DEBUG) {
                    System.out.println("**** Trying to do restart from reinstalling the application....");
                }
                if (!selectedDevice.equals("WiFi")) { // Cannot restart WiFi app, but it is likely just fine.
                    doReinstallApplication();
                    doRestartApplication();
                }
                restarted = true;
                establishSocketLevelCommunication();
                doWrite(message);
                connectionHappy = true;
                return;
            } catch (IOException e) {
                if (DEBUG) {
                    System.out.println("**** Reinstall was not enough: " + e.getMessage());
                }
                if (restarted) {
                    androidController.androidRestartBridge();
                    // try exponential back-off for opening the socket
                    int sleepTime = 3000; // start with 3 seconds
                    for (int i = 1; i <= 4; i++) { // 4 tries at most = 45 sec. total
                        try {
                            if (DEBUG) {
                                System.out.println("     ... waiting " + sleepTime / 1000
                                        + " seconds and then trying again to establish connection");
                            }
                            Thread.sleep(sleepTime);
                            establishSocketLevelCommunication();
                            doWrite(message);
                            connectionHappy = true;
                            return;
                        } catch (Exception e2) {
                            if (i == 4) {
                                if (DEBUG) {
                                    System.out.println("**** giving up on connecting to phone: " + e2.getMessage());
                                }
                            } else {
                                if (DEBUG) {
                                    System.out.println("Failed to connect: " + e2.getMessage());
                                }
                                sleepTime = sleepTime * 2; // double and try again
                            }
                        }
                    }
                }
                failedMessage = e.getMessage();
            }
        } else {
            if (DEBUG) {
                System.out.println("***** No device selected");
            }
            throw new IOException("It appears that no device has been selected");
        }
        connectionHappy = false;

        // there was nothing plugged in, or somehow we got unplugged by now
        if (devicesPluggedIn == 0) {
            String errorMessage = "App Inventor cannot find any connected devices.\n";
            throw new IOException(errorMessage);
        }

        if (DEBUG) {
            System.out.println("**** devicesPluggedIn: " + devicesPluggedIn + " selectedDevice: " + selectedDevice
                    + " connectionHappy: " + connectionHappy);
        }

        throw new IOException(failedMessage);
    }

    /**
     * Send a string to the REPL controller.  Expects that the controller is
     * already happily running.
     * @param message the string to send to the REPL controller
     * @throws IOException
     */
    public void send(String message) throws IOException {
        if (DEBUG) {
            System.out.println("ReplController sending " + message);
        }
        // TODO(kerr): how deal with threading with respect to devices plugged in?

        if (connectionHappy) {
            // we _think_ that it is connected...
            doWrite(message);
        } else {
            throw new IOException("Connection is not happy and needs restart.");
        }
    }

    private void establishSocketLevelCommunication() throws IOException {
        // Unfortunately, need to reforward the port for every new socket connection.
        reset();
        if (!forwardAndroidPort()) {
            throw new IOException("Could not forward TCP port.");
        }
        if (DEBUG) {
            System.out.println("trying to establish communication");
        }
        try {
            setupSocket();
        } catch (IOException e) {
            if (DEBUG) {
                System.out.println("Failed in setupSocket: " + e.getMessage());
            }
            throw e;
        }
    }

    private void setupSocket() throws IOException {
        if (inputThread != null) {
            inputThread.stopRunning();
        }
        socket = new Socket(host, port);
        // Note that this call does not fail even if there's nothing running on
        // the other end.  Also, there's no problem getting the streams.  Sigh.
        out = socket.getOutputStream();
        in = socket.getInputStream();
        int ch = in.read(); // We'll put it back before the thread tries to read it.
        if (ch < 0) {
            throw new IOException("Input stream closed before found anything on it.");
        }
        inputThread = new ReadWriteThread(ch);
        inputThread.start();
    }

    private boolean forwardAndroidPort() {
        boolean result = androidController.androidForwardTcpPort(PhoneCommManager.REPL_COMMUNICATION_PORT,
                PhoneCommManager.REPL_COMMUNICATION_PORT);
        if (DEBUG) {
            if (result) {
                System.out.println("ADB forward command was successfully run");
            } else {
                System.out.println("ADB forward command failed to run");
            }
        }
        return result;
    }

    /**
     * This function can take as long as 10 seconds since it sleeps (in .1
     * second increments) while waiting for the application to restart.
     * Usually does not take more than two or three seconds, sometimes much
     * less.
     */
    private void doRestartApplication() throws IOException {
        try {
            androidController.androidKillStarterApplication();
            androidController.androidStartStarterApplication();
            int attempts = 0;
            while (attempts < 10) {
                // Unfortunately, the test if the application is running is not as
                // good as a test if it is actually responding.
                Thread.sleep(1000);
                if (androidController.androidIsStarterApplicationRunning()) {
                    return;
                }
                attempts++;
            }
            // Sometimes we can't tell if it's running but it seems to be doing
            // so.  We'll catch the error later if it really didn't start.
            if (DEBUG) {
                System.out.println("Allegedly failed to start application - waited too long.");
            }
        } catch (AndroidControllerException e) {
            throw new IOException("Failed to start application: " + e.getMessage());
        } catch (java.lang.InterruptedException e) {
            throw new IOException("Failed to start application: " + e.getMessage());
        }
    }

    private void doReinstallApplication() throws IOException, ExternalStorageException {
        if (DEBUG) {
            System.out.println("Trying to sync and install on the device...");
        }
        try {
            androidController.androidSyncAndInstallStarterApplication();
        } catch (AndroidControllerException e) {
            throw new IOException(e.getMessage());
        }
    }

    public void reset() {
        try {
            if (inputThread != null) {
                inputThread.stopRunning();
            }
            if (socket != null) {
                socket.close();
                socket = null;
            }
        } catch (IOException e) {
            if (DEBUG) {
                System.out.println("Problem closing REPL socket");
            }
        }
    }

    public void deviceConnected(String serialNumber) {
        devicesPluggedIn++;
        if (DEBUG) {
            System.out.println("Device connected: " + devicesPluggedIn + " devices plugged in.");
        }
        postProcessor.onConnect(serialNumber);
    }

    public void deviceDisconnected(String serialNumber) {
        devicesPluggedIn--;

        if (DEBUG) {
            System.out.println("Device disconnected: " + devicesPluggedIn + " devices plugged in.");
        }
        if (devicesPluggedIn < 0) {
            if (DEBUG) {
                System.out.println("XXXXXX We're confused about number of connected devices. Yikes!");
            }
            devicesPluggedIn = 0;
        } else if (serialNumber.equals(androidController.getSelectedDevice())) {
            // If the user unplugged the device we were connected to, we care.  Otherwise not.
            connectionHappy = false;
        }
        postProcessor.onDisconnect(serialNumber);
    }

    public String getSelectedDevice() {
        return androidController.getSelectedDevice();
    }

    public boolean selectDevice(String device, String ipAddress) {
        try {
            if (device.equals("WiFi")) {
                this.host = ipAddress;
                androidController.selectDevice(device, ipAddress);
                phoneManager.replWifiStart();
            } else {
                this.host = "127.0.0.1"; // The IP address used for USB connected phones
                androidController.selectDevice(device);
            }
            return true;
        } catch (AndroidControllerException e) {
            return false;
        }
    }

    public boolean selectDevice(String device) {
        try {
            if (device.equals("WiFi")) {
                rendevzousIpAddress(); // This runs in another thread which calls selectDevice(device, ipaddress) when ready
                return false;
            } else {
                androidController.selectDevice(device);
            }
            return true;
        } catch (AndroidControllerException e) {
            return false;
        }
    }

    class ReadWriteThread extends Thread {

        private volatile boolean shouldRun = true;
        private volatile int initialCh = -1;

        ReadWriteThread(int ch) {
            super();
            initialCh = ch;
        }

        public void stopRunning() {
            this.shouldRun = false;
        }

        @Override
        public void run() {
            try {
                byte[] buffer = new byte[1024];
                while (shouldRun) {
                    int ch;
                    if (initialCh != -1) {
                        ch = initialCh;
                        initialCh = -1;
                    } else {
                        ch = in.read();
                    }
                    if (ch < 0)
                        break; // ??? FIXME
                    buffer[0] = (byte) ch;
                    int avail = in.available();
                    if (avail > 0) {
                        if (avail > buffer.length - 1)
                            avail = buffer.length - 1;
                        avail = in.read(buffer, 1, avail);
                    }
                    String received = new String(buffer, 0, avail + 1, "UTF-8");
                    if (postProcessor != null) {
                        postProcessor.postProcess(received);
                    }
                }
            } catch (java.io.IOException ex) {
                if (shouldRun) {
                    // exception wasn't due to us being reset
                    if (DEBUG) {
                        System.out.println("ReadWriteThread got exception " + ex);
                        ex.printStackTrace();
                    }
                    postProcessor.onFailure(ex);
                }
            }
        }
    }

    private void rendevzousIpAddress() {
        // Generate the random 6 digit code and the QRCode that contains it.
        String AB = "abcdefghijklmnopqrstuvwxyz";
        Random rnd = new Random();
        StringBuilder sb = new StringBuilder(6);
        for (int i = 0; i < 6; i++)
            sb.append(AB.charAt(rnd.nextInt(AB.length())));
        final String code = sb.toString();
        final ImageIcon qrcode = generateQRCode(code);
        stopWireless = false; // Re-initialize in case it was set in a previous attempt
        final JDialog displayedCode = FeedbackReporter.showWirelessCodeDialog(code, qrcode, new Runnable() {
            public void run() {
                setStopWireless();
            }
        });
        Thread t = new Thread(new Runnable() {
            public void run() {
                try {
                    // Put up the wireless dialog box
                    Thread.sleep(4000); // Sleep 4 seconds, time to give the user a chance to start the Companion App
                    String jsonString = fetchJsonString(code);
                    int count = 0;
                    while (jsonString == null) {
                        if (count++ > 56) { // This limits this loop to about a minute (4 seconds wait above and 56 seconds round the loop)
                            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    displayedCode.setVisible(false); // Take down pending message
                                    FeedbackReporter.showErrorMessage(
                                            "We failed to find your phone, please try again.", "Try Again");
                                }
                            });
                            return;
                        }
                        if (stopWireless) {
                            javax.swing.SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    displayedCode.setVisible(false); // Take down pending message
                                }
                            });
                            return;
                        }
                        Thread.sleep(1000); // Pause a second (so the total wait time for the first loop is really 5 seconds subsequent loops are 1 second
                        if (DEBUG)
                            System.out.println("DeviceReplCommController (WiFi Connect) looping count = " + count);
                        jsonString = fetchJsonString(code);
                    }
                    JSONObject jsonObject = new JSONObject(jsonString);
                    final String ipAddress = (String) jsonObject.get("ipaddr");
                    if (DEBUG)
                        System.out.println("Got ipaddr = " + ipAddress);

                    // We have the IP address, we now send our version to the phone which
                    // starts the phone TelnetRepl listening. If this version doesn't match
                    // The phone will display an error and not listen. We don't need to know
                    // the result because if the phone fails to listen it will reject the
                    // connection that is attempted when we call selectDevice()
                    String curl = "http://" + ipAddress + ":8000/_version?version="
                            + YaVersion.YOUNG_ANDROID_VERSION;
                    if (DEBUG)
                        System.out.println("Connecting to: " + curl);
                    URL url = new URL(curl);
                    URLConnection con = null;
                    try {
                        con = url.openConnection();
                        con.getInputStream().close(); // We don't care about the return value
                    } catch (FileNotFoundException fnf) {
                        System.out.println("Exception setting version, ignoring for now.");
                        fnf.printStackTrace(System.out); // Let's not hide it though!
                    }
                    javax.swing.SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            displayedCode.setVisible(false);
                            selectDevice("WiFi", ipAddress); // This is run on the UI thread
                        }
                    });
                } catch (Exception e) {
                    System.out.println("It did not work." + e.toString());//return
                    e.printStackTrace(System.out);
                    javax.swing.SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            if (displayedCode != null)
                                displayedCode.setVisible(false);
                        }
                    });
                }
            }
        });
        t.start();
    }

    private ImageIcon generateQRCode(String code) {
        Charset charset = Charset.forName("ISO-8859-1");
        CharsetEncoder encoder = charset.newEncoder();
        byte[] b = null;
        try {
            // Convert a string to ISO-8859-1 bytes in a ByteBuffer
            ByteBuffer bbuf = encoder.encode(CharBuffer.wrap(code));
            b = bbuf.array();
        } catch (CharacterCodingException e) {
            System.out.println(e.getMessage());
        }

        String data = null;
        try {
            data = new String(b, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            System.out.println(e.getMessage());
        }

        // get a byte matrix for the data
        BitMatrix matrix = null;
        int h = 200;
        int w = 200;
        com.google.zxing.Writer writer = new QRCodeWriter();
        try {
            matrix = writer.encode(data, com.google.zxing.BarcodeFormat.QR_CODE, w, h);
            ImageIcon qrcode = new ImageIcon();
            qrcode.setImage(MatrixToImageWriter.toBufferedImage(matrix));
            return qrcode;
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return null;
    }

    private String fetchJsonString(String code) throws IOException {
        URL url = new URL(rendezvousUrl + code); //
        URLConnection con = url.openConnection();
        InputStream in = con.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        return (reader.readLine());
    }

    private void setStopWireless() {
        stopWireless = true;
    }

    public void setPhoneManager(PhoneCommManager phoneManager) {
        this.phoneManager = phoneManager;
    }

}