org.asnelt.derandom.ProcessingFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.asnelt.derandom.ProcessingFragment.java

Source

/*
 * Copyright (C) 2015 Arno Onken
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.asnelt.derandom;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.Fragment;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This class implements a fragment for doing generator related processing. The fragment is retained
 * across configuration changes.
 */
public class ProcessingFragment extends Fragment {
    /**
     * Interface for listening to processing changes.
     */
    public interface ProcessingFragmentListener {
        /**
         * Called when the history prediction was completely replaced.
         * @param historyNumbers previously entered numbers
         * @param historyPredictionNumbers predictions for previous numbers
         */
        void onHistoryPredictionReplaced(long[] historyNumbers, long[] historyPredictionNumbers);

        /**
         * Called when the random number generator selection changed.
         * @param generatorIndex index of new generator
         */
        void onGeneratorChanged(int generatorIndex);

        /**
         * Called when the input history changed.
         * @param inputNumbers the entered numbers
         * @param predictionNumbers predictions for entered numbers
         */
        void onHistoryChanged(long[] inputNumbers, long[] predictionNumbers);

        /**
         * Called when the predictions for upcoming numbers changed.
         * @param predictionNumbers predictions of upcoming numbers
         */
        void onPredictionChanged(long[] predictionNumbers);

        /**
         * Called when setting the input method to an input file is aborted and sets the input
         * method back to direct input.
         */
        void onFileInputAborted();

        /**
         * Called when setting the input method to an input socket is aborted and sets the input
         * method back to direct input.
         */
        void onSocketInputAborted();

        /**
         * Called when invalid numbers where entered.
         */
        void onInvalidInputNumber();

        /**
         * Called when the input was cleared.
         */
        void onClear();

        /**
         * Called when the progress status changed.
         */
        void onProgressUpdate();

        /**
         * Called when the status of the network socket changed.
         * @param newStatus a description of the new status
         */
        void onSocketStatusChanged(String newStatus);
    }

    /** Random manager for generating predictions. */
    private final RandomManager randomManager;
    /** Handler for updating the user interface. */
    private final Handler handler;
    /** Circular buffer for storing input numbers. */
    private final HistoryBuffer historyBuffer;
    /** Object for synchronizing the main thread and the processing thread. */
    private final Object synchronizationObject;
    /** Executor service for all processing tasks. */
    private final ExecutorService processingExecutor;
    /** Executor service for server task. */
    private final ExecutorService serverExecutor;
    /** Lock for the disconnected condition. */
    private final Lock connectionLock;
    /** Condition that is triggered when the client socket is disconnected. */
    private final Condition disconnected;
    /** Number of numbers to forecast. */
    private volatile int predictionLength;
    /** Flag for whether the generator should be detected automatically. */
    private volatile boolean autoDetect;
    /** Current input file for reading numbers. */
    private volatile Uri inputUri;
    /** Reader for reading input numbers. */
    private volatile BufferedReader inputReader;
    /** Writer for writing predictions to the client socket. */
    private volatile BufferedWriter outputWriter;
    /** Flag for whether a user interface update was missed during a configuration change. */
    private volatile boolean missingUpdate;
    /** Number of process input tasks. */
    private volatile int inputTaskLength;
    /** Flag for whether processing should continue. */
    private volatile boolean processingDesirable;
    /** Server socket port. */
    private volatile int serverPort;
    /** Server socket. */
    private ServerSocket serverSocket;
    /** Client socket. */
    private volatile Socket clientSocket;
    /** Listener for processing changes. */
    private ProcessingFragmentListener listener;
    /** Future for cancelling the server task. */
    private Future<?> serverFuture;
    /** Index of selected input method. */
    private int inputSelection;

    /**
     * Constructor for initializing the processing fragment. Generates a HistoryBuffer, a
     * RandomManager and an ExecutorService.
     */
    public ProcessingFragment() {
        super();
        predictionLength = 0;
        autoDetect = false;
        inputUri = null;
        inputReader = null;
        outputWriter = null;
        missingUpdate = false;
        inputSelection = 0;
        inputTaskLength = 0;
        processingDesirable = true;
        serverPort = 0;
        clientSocket = null;
        synchronizationObject = this;
        historyBuffer = new HistoryBuffer(0);
        randomManager = new RandomManager();
        // Handler for processing user interface updates
        handler = new Handler(Looper.getMainLooper());
        processingExecutor = Executors.newSingleThreadExecutor();
        serverExecutor = Executors.newSingleThreadExecutor();
        connectionLock = new ReentrantLock();
        disconnected = connectionLock.newCondition();
        serverFuture = null;
    }

    /**
     * Initializes this activity. Called only once since the fragment is retained across
     * configuration changes.
     * @param savedInstanceState Bundle with saved state
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Retain fragment across configuration changes
        setRetainInstance(true);
    }

    /**
     * Called when the fragment is associated with an activity.
     * @param context the context the fragment is associated with
     */
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof MainActivity) {
            listener = (ProcessingFragmentListener) context;
        }
    }

    /**
     * Called before the fragment is no longer associated with an activity.
     */
    @Override
    public void onDetach() {
        super.onDetach();
        listener = null;
    }

    /**
     * Sets the currently active generator.
     * @param number index of the currently active generator
     */
    public void setCurrentGenerator(int number) {
        if (number != randomManager.getCurrentGenerator()) {
            prepareInputProcessing();
            processingExecutor.execute(new UpdateAllTask(number));
        }
    }

    /**
     * Returns human readable names of all generators.
     * @return all generator names
     */
    public String[] getGeneratorNames() {
        return randomManager.getGeneratorNames();
    }

    /**
     * Returns the name of the currently active generator.
     * @return name of the currently active generator
     */
    public String getCurrentGeneratorName() {
        return randomManager.getCurrentGeneratorName();
    }

    /**
     * Returns the parameter names of the currently active generator.
     * @return all parameter names of the currently active generator
     */
    public String[] getCurrentParameterNames() {
        return randomManager.getCurrentParameterNames();
    }

    /**
     * Returns all parameter values of the currently active generator.
     * @return parameter values of the currently active generator
     */
    public long[] getCurrentParameters() {
        return randomManager.getCurrentParameters();
    }

    /**
     * Sets the current input selection index.
     * @param inputSelection the new input selection index
     */
    public void setInputSelection(int inputSelection) {
        this.inputSelection = inputSelection;
    }

    /**
     * Returns the current input selection index.
     * @return the current input selection index
     */
    public int getInputSelection() {
        return inputSelection;
    }

    /**
     * Returns the current input URI or null if no input URI is set.
     * @return the current input URI
     */
    public Uri getInputUri() {
        return inputUri;
    }

    /**
     * Sets the input URI to null.
     */
    public void resetInputUri() {
        inputUri = null;
    }

    /**
     * Sets the number of numbers to predict.
     * @param predictionLength the number of numbers to predict
     */
    public void setPredictionLength(int predictionLength) {
        if (this.predictionLength != predictionLength) {
            this.predictionLength = predictionLength;
            updatePrediction();
        }
    }

    /**
     * Sets the flag that determines whether the generator is detected automatically..
     * @param autoDetect automatically detect generator if true
     */
    public void setAutoDetect(boolean autoDetect) {
        this.autoDetect = autoDetect;
    }

    /**
     * Sets the server port. If a server task is running, then it is restarted.
     * @param serverPort the new server port
     */
    public void setServerPort(int serverPort) {
        if (this.serverPort != serverPort) {
            this.serverPort = serverPort;
            if (serverFuture != null) {
                stopServerTask();
                startServerTask();
            }
        }
    }

    /**
     * Determines whether input is currently processed.
     * @return true if input is currently processed
     */
    public boolean isProcessingInput() {
        return inputTaskLength > 0;
    }

    /**
     * Determines whether a user interface update was missed during a configuration change.
     * @return true if an update was missed
     */
    public boolean isMissingUpdate() {
        return missingUpdate;
    }

    /**
     * Executes a clear task.
     */
    public void clear() {
        processingDesirable = false;
        processingExecutor.execute(new ClearTask());
    }

    /**
     * Executes a change capacity task.
     * @param capacity the new input history capacity
     */
    public void setCapacity(int capacity) {
        processingExecutor.execute(new ChangeCapacityTask(capacity));
    }

    /**
     * Executes an update task.
     */
    public void updateAll() {
        prepareInputProcessing();
        processingExecutor.execute(new UpdateAllTask());
    }

    /**
     * Processes an input string of newline separated integers and calculates a prediction.
     * @param input the input string to be processed
     */
    public void processInputString(String input) {
        // Process input in separate thread
        prepareInputProcessing();
        processingExecutor.execute(new ProcessInputTask(input));
    }

    /**
     * Executes a process input task with an input reader.
     */
    public void processInputSocket() {
        // Process input in separate thread
        prepareInputProcessing();
        processingExecutor.execute(new ProcessInputTask());
    }

    /**
     * Opens and processes the input file pointed to by fileUri. Disables direct input.
     * @param fileUri the URI of the file to be processed
     */
    public void processInputFile(Uri fileUri) {
        // Process input in separate thread
        prepareInputProcessing();
        processingExecutor.execute(new ProcessInputTask(fileUri));
    }

    /**
     * Starts the server task and sets the server future.
     */
    public void startServerTask() {
        serverFuture = serverExecutor.submit(new ServerTask());
    }

    /**
     * Stops the server task and sets the server future to null. Closes all sockets.
     */
    public void stopServerTask() {
        if (serverFuture != null) {
            serverFuture.cancel(true);
            serverFuture = null;
        }
        closeSockets();
    }

    /**
     * Closes the client socket and the reader and writer.
     */
    private void closeClient() {
        synchronized (synchronizationObject) {
            if (inputReader != null) {
                try {
                    inputReader.close();
                } catch (IOException e) {
                    // We do not need to do anything more with the reader
                }
            }
            if (outputWriter != null) {
                try {
                    outputWriter.close();
                } catch (IOException e) {
                    // We do not need to do anything more with the writer
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    // We do not need to do anything more with the client socket
                }
                clientSocket = null;
            }
        }
    }

    /**
     * Closes all sockets.
     */
    private void closeSockets() {
        synchronized (synchronizationObject) {
            closeClient();
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // We do not need to do anything more with the server socket
                }
                serverSocket = null;
            }
        }
    }

    /**
     * Called when the activity is destroyed.
     */
    @Override
    public void onDestroy() {
        processingDesirable = false;
        // Shutdown server thread
        serverExecutor.shutdownNow();
        // Shutdown processing thread
        processingExecutor.shutdownNow();
        // Close all sockets
        closeSockets();
        super.onDestroy();
    }

    /**
     * Prepares input processing by incrementing the counter of active input tasks and updating the
     * progressing status.
     */
    private void prepareInputProcessing() {
        synchronized (synchronizationObject) {
            inputTaskLength++;
        }
        if (listener != null) {
            listener.onProgressUpdate();
        }
    }

    /**
     * Finishes input processing by decrementing the counter of active input tasks.
     */
    private void finishInputProcessing() {
        synchronized (synchronizationObject) {
            if (inputTaskLength > 0) {
                inputTaskLength--;
            }
        }
    }

    /**
     * Calculates a new prediction and notifies the listener.
     */
    private void updatePrediction() {
        processingExecutor.execute(new UpdatePredictionTask());
    }

    /**
     * This class implements a task for clearing all input.
     */
    private class ClearTask implements Runnable {
        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            historyBuffer.clear();
            int currentGenerator = randomManager.getCurrentGenerator();
            randomManager.reset();
            randomManager.setCurrentGenerator(currentGenerator);
            boolean posted = handler.post(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        listener.onClear();
                        missingUpdate = false;
                    } else {
                        missingUpdate = true;
                    }
                }
            });
            if (!posted) {
                missingUpdate = true;
            }
            processingDesirable = true;
        }
    }

    /**
     * This class implements a task for changing the input history capacity.
     */
    private class ChangeCapacityTask implements Runnable {
        /** The new input capacity. */
        private final int capacity;

        /**
         * Constructor for setting the new input capacity.
         */
        public ChangeCapacityTask(final int capacity) {
            this.capacity = capacity;
        }

        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            historyBuffer.setCapacity(capacity);
        }
    }

    /**
     * This class implements a task for updating all history predictions and predictions.
     */
    private class UpdateAllTask implements Runnable {
        /** The index of the new generator. */
        private final int generatorIndex;
        /** Flag that determines whether the generator should be changed. */
        private final boolean changeGenerator;

        /**
         * Standard constructor that initializes a task that does not change the generator..
         */
        public UpdateAllTask() {
            this.generatorIndex = 0;
            this.changeGenerator = false;
        }

        /**
         * Constructor that initializes a task that does change the generator.
         * @param generatorIndex index of the new generator
         */
        public UpdateAllTask(final int generatorIndex) {
            this.generatorIndex = generatorIndex;
            this.changeGenerator = true;
        }

        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            final boolean generatorChanged;
            if (changeGenerator && randomManager.getCurrentGenerator() != generatorIndex) {
                // Process complete history
                randomManager.setCurrentGenerator(generatorIndex);
                generatorChanged = true;
            } else {
                generatorChanged = false;
            }
            final long[] historyNumbers;
            final long[] historyPredictionNumbers;
            final long[] predictionNumbers;
            if ((generatorChanged || !changeGenerator) && historyBuffer.length() > 0) {
                randomManager.resetCurrentGenerator();
                randomManager.findCurrentSeries(historyBuffer.toArray(), null);
                historyNumbers = historyBuffer.toArray();
                historyPredictionNumbers = randomManager.getIncomingPredictionNumbers();
                // Generate new prediction without updating the state
                predictionNumbers = randomManager.predict(predictionLength);
            } else {
                historyNumbers = null;
                historyPredictionNumbers = null;
                predictionNumbers = null;
            }
            finishInputProcessing();
            boolean posted = handler.post(new Runnable() {
                @Override
                public void run() {
                    if (generatorChanged || !changeGenerator) {
                        if (listener != null) {
                            listener.onProgressUpdate();
                            listener.onHistoryPredictionReplaced(historyNumbers, historyPredictionNumbers);
                            listener.onPredictionChanged(predictionNumbers);
                            missingUpdate = false;
                        } else {
                            missingUpdate = true;
                        }
                    }
                }
            });
            if (!posted) {
                missingUpdate = true;
            }
        }
    }

    /**
     * This class implements a task that processes input numbers.
     */
    private class ProcessInputTask implements Runnable {
        /** Input string of newline separated integers. */
        private final String input;
        /** File input URI. */
        private final Uri fileUri;

        /**
         * Constructor for processing an input string.
         * @param input the input string to be processed
         */
        public ProcessInputTask(final String input) {
            this.input = input;
            this.fileUri = null;
        }

        /**
         * Constructor for processing the input file pointed to by fileUri.
         * @param fileUri the URI of the file to be processed
         */
        public ProcessInputTask(final Uri fileUri) {
            this.input = null;
            this.fileUri = fileUri;
        }

        /**
         * Constructor for processing input from current input reader.
         */
        public ProcessInputTask() {
            this.input = null;
            this.fileUri = null;
        }

        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            try {
                if (input != null) {
                    processInputString(input);
                } else if (fileUri != null) {
                    processFileInput();
                } else if (inputReader != null) {
                    processSocketInput();
                }
            } catch (NumberFormatException e) {
                // Call listener for showing error message
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        if (listener != null) {
                            listener.onInvalidInputNumber();
                        }
                    }
                });
            }
            finishInputProcessing();
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        listener.onProgressUpdate();
                    }
                }
            });
        }

        /**
         * Opens the input reader from the inputUri and initializes processing of the reader.
         */
        private void processFileInput() {
            inputUri = fileUri;
            try {
                InputStream stream = getActivity().getContentResolver().openInputStream(inputUri);
                if (stream == null) {
                    throw new NullPointerException();
                }
                inputReader = new BufferedReader(new InputStreamReader(stream));
            } catch (FileNotFoundException | NullPointerException e) {
                abortFileInput();
                return;
            }
            try {
                while (inputReader.ready() && processingDesirable) {
                    String nextInput = inputReader.readLine();
                    if (nextInput == null) {
                        break;
                    }
                    processInputString(nextInput);
                }
                inputReader.close();
            } catch (IOException | NullPointerException e) {
                abortFileInput();
            }
            try {
                inputReader.close();
            } catch (IOException e) {
                // We do not need to do anything more with this inputReader
            }
            inputReader = null;
        }

        /**
         * Reads input from the input reader and assembles an input string to be processed.
         */
        private void processSocketInput() {
            connectionLock.lock();
            try {
                while (clientSocket != null && !clientSocket.isClosed()) {
                    String nextInput = inputReader.readLine();
                    if (nextInput == null) {
                        break;
                    }
                    try {
                        processInputString(nextInput);
                    } catch (NumberFormatException e) {
                        // Call listener for showing error message
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (listener != null) {
                                    listener.onInvalidInputNumber();
                                }
                            }
                        });
                    }
                }
                disconnected.signal();
            } catch (IOException | NullPointerException e) {
                // Ignore exception
            } finally {
                connectionLock.unlock();
            }
        }

        /**
         * Aborts file input processing and updates the user interface.
         */
        private void abortFileInput() {
            if (inputReader != null) {
                try {
                    inputReader.close();
                } catch (IOException e) {
                    // We do not need to do anything more with this inputReader
                }
                inputReader = null;
            }
            if (inputUri != null) {
                inputUri = null;
            }
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        // Abort file input processing
                        listener.onFileInputAborted();
                    }
                }
            });
        }

        /**
         * Processes the given input string by parsing the numbers and searching for compatible
         * generator states. The generator is eventually changed if the flag autoDetect is set and a
         * better generator is detected..
         * @param input string of newline separated integers
         * @throws NumberFormatException if input contains an invalid number string
         */
        private void processInputString(String input) throws NumberFormatException {
            long[] inputNumbers;
            String[] stringNumbers = input.split("\n");
            inputNumbers = new long[stringNumbers.length];
            // Parse numbers
            for (int i = 0; i < inputNumbers.length; i++) {
                inputNumbers[i] = Long.parseLong(stringNumbers[i]);
            }
            long[] historyPredictionNumbers;
            long[] historyNumbers = null;
            long[] replacedNumbers = null;
            int bestGenerator = 0;
            boolean generatorChanged = false;
            if (autoDetect) {
                // Detect best generator and update all states
                bestGenerator = randomManager.detectGenerator(inputNumbers, historyBuffer);
                historyPredictionNumbers = randomManager.getIncomingPredictionNumbers();
                if (bestGenerator != randomManager.getCurrentGenerator()) {
                    // Set generator and process complete history
                    randomManager.setCurrentGenerator(bestGenerator);
                    randomManager.resetCurrentGenerator();
                    historyNumbers = historyBuffer.toArray();
                    randomManager.findCurrentSeries(historyNumbers, null);
                    replacedNumbers = randomManager.getIncomingPredictionNumbers();
                    randomManager.findCurrentSeries(inputNumbers, historyBuffer);
                    historyPredictionNumbers = randomManager.getIncomingPredictionNumbers();
                    generatorChanged = true;
                }
            } else {
                randomManager.findCurrentSeries(inputNumbers, historyBuffer);
                historyPredictionNumbers = randomManager.getIncomingPredictionNumbers();
            }
            // Generate new prediction without updating the state
            long[] predictionNumbers = randomManager.predict(predictionLength);
            historyBuffer.put(inputNumbers);
            // Post result to user interface
            if (generatorChanged) {
                showGeneratorChange(inputNumbers, historyPredictionNumbers, predictionNumbers, historyNumbers,
                        replacedNumbers, bestGenerator);
            } else {
                showInputUpdate(inputNumbers, historyPredictionNumbers, predictionNumbers);
            }
        }

        /**
         * Sends the processing result to the processing listener.
         * @param inputNumbers the processed input numbers
         * @param historyPredictionNumbers the prediction numbers corresponding to the input
         * @param predictionNumbers the predicted numbers
         */
        private void showInputUpdate(final long[] inputNumbers, final long[] historyPredictionNumbers,
                final long[] predictionNumbers) {
            boolean posted = handler.post(new Runnable() {
                @Override
                public void run() {
                    // Append input numbers to history
                    if (listener != null) {
                        listener.onHistoryChanged(inputNumbers, historyPredictionNumbers);
                        listener.onPredictionChanged(predictionNumbers);
                    } else {
                        missingUpdate = true;
                    }
                }
            });
            if (!posted) {
                missingUpdate = true;
            }
            writeSocketOutput(predictionNumbers);
        }

        /**
         * Sends the processing result to the processing listener. The result includes a change of
         * generator.
         * @param inputNumbers the processed input numbers
         * @param historyPredictionNumbers the prediction numbers corresponding to the input
         * @param predictionNumbers the predicted numbers
         * @param historyNumbers the complete previous input
         * @param replacedNumbers the complete previous prediction numbers
         * @param bestGenerator index of the best generator
         */
        private void showGeneratorChange(final long[] inputNumbers, final long[] historyPredictionNumbers,
                final long[] predictionNumbers, final long[] historyNumbers, final long[] replacedNumbers,
                final int bestGenerator) {
            boolean posted = handler.post(new Runnable() {
                @Override
                public void run() {
                    // Append input numbers to history
                    if (listener != null) {
                        listener.onGeneratorChanged(bestGenerator);
                        listener.onHistoryPredictionReplaced(historyNumbers, replacedNumbers);
                        listener.onHistoryChanged(inputNumbers, historyPredictionNumbers);
                        listener.onPredictionChanged(predictionNumbers);
                    } else {
                        missingUpdate = true;
                    }
                }
            });
            if (!posted) {
                missingUpdate = true;
            }
            writeSocketOutput(predictionNumbers);
        }

        /**
         * Writes the prediction numbers to the client socket. Writes an additional newline after
         * the prediction block.
         * @param predictionNumbers the predicted numbers
         */
        private void writeSocketOutput(long[] predictionNumbers) {
            if (outputWriter != null && predictionNumbers != null) {
                try {
                    // Write numbers to output stream
                    for (long number : predictionNumbers) {
                        outputWriter.write(Long.toString(number));
                        outputWriter.newLine();
                    }
                    // Finish this sequence of numbers with an additional newline
                    outputWriter.newLine();
                    outputWriter.flush();
                } catch (IOException | NullPointerException e) {
                    // Ignore unsuccessful writes
                }
            }
        }
    }

    /**
     * This class implements a task for updating the generator prediction.
     */
    private class UpdatePredictionTask implements Runnable {
        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            // Generate new prediction without updating the state
            final long[] predictionNumbers;
            if (historyBuffer.length() > 0) {
                predictionNumbers = randomManager.predict(predictionLength);
            } else {
                predictionNumbers = null;
            }
            boolean posted = handler.post(new Runnable() {
                @Override
                public void run() {
                    if (!missingUpdate && listener != null) {
                        listener.onPredictionChanged(predictionNumbers);
                    } else {
                        missingUpdate = true;
                    }
                }
            });
            if (!posted) {
                missingUpdate = true;
            }
        }
    }

    /**
     * This class implements a task for establishing a socket connection.
     */
    private class ServerTask implements Runnable {
        /**
         * Starts executing the code of the task.
         */
        @Override
        public void run() {
            connectionLock.lock();
            try {
                try {
                    serverSocket = new ServerSocket(serverPort);
                } catch (IOException e) {
                    abortSocketInput();
                    return;
                }
                while (!Thread.currentThread().isInterrupted()) {
                    String status;
                    if (clientSocket == null || clientSocket.isClosed()) {
                        try {
                            // Display information about the server socket
                            status = getResources().getString(R.string.server_listening) + " "
                                    + Integer.toString(serverPort);
                            postStatus(status);
                            clientSocket = serverSocket.accept();
                            status = getResources().getString(R.string.client_connected);
                            postStatus(status);
                            InputStream inputStream = clientSocket.getInputStream();
                            inputReader = new BufferedReader(new InputStreamReader(inputStream));
                            OutputStream outputStream = clientSocket.getOutputStream();
                            outputWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
                        } catch (IOException e) {
                            closeClient();
                            continue;
                        }
                        boolean posted = handler.post(new Runnable() {
                            @Override
                            public void run() {
                                processInputSocket();
                            }
                        });
                        if (!posted) {
                            abortSocketInput();
                            return;
                        }
                    }
                    try {
                        disconnected.await();
                        closeClient();
                        status = getResources().getString(R.string.client_disconnected);
                        postStatus(status);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            } finally {
                connectionLock.unlock();
                closeSockets();
            }
        }

        /**
         * Posts the current socket status to the user interface thread.
         * @param status the status message
         */
        private void postStatus(final String status) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        listener.onSocketStatusChanged(status);
                    }
                }
            });
        }

        /**
         * Aborts socket input processing and updates the user interface.
         */
        private void abortSocketInput() {
            closeSockets();
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (listener != null) {
                        listener.onSocketInputAborted();
                    }
                }
            });
        }
    }
}