public class DigitView extends View {
    private static final int OVERLAY_TIME = 1;
    private static final int DIGIT_SIDE = 28;

    private final Bitmap mDigitBitmap;
    private final Canvas mDigitCanvas;
    private final Matrix mNarrowMatrix;
    private final Matrix mEnlargeMatrix;
    private final Paint mBgPaint;
    private final Paint mFgPaint;
    private final Paint mRicePaint;

    private final Pair mOldPair;
    private final Pair mNewPair;
    private final Path mRicePath;

    private boolean mIsTrash;

    {
        mDigitBitmap = Bitmap.createBitmap(DIGIT_SIDE, DIGIT_SIDE, Bitmap.Config.ARGB_8888);
        mDigitCanvas = new Canvas(mDigitBitmap);
        mNarrowMatrix = new Matrix();
        mEnlargeMatrix = new Matrix();

        mBgPaint = new Paint();
        mBgPaint.setColor(Color.WHITE);

        mFgPaint = new Paint();
        mFgPaint.setColor(Color.BLACK);
        mFgPaint.setAntiAlias(true);
        mFgPaint.setStrokeWidth(2.5F);

        mRicePaint = new Paint();
        mRicePaint.setColor(Color.BLACK);
        mRicePaint.setAlpha(85);
        mRicePaint.setPathEffect(new DashPathEffect(new float[]{15, 15, 15, 15}, 1));
        mRicePaint.setAntiAlias(true);
        mRicePaint.setStyle(Paint.Style.STROKE);
        mRicePaint.setStrokeWidth(3);

        mOldPair = new Pair();
        mNewPair = new Pair();

        mRicePath = new Path();

        mIsTrash = false;
    }

    public DigitView(Context context) {
        super(context);
    }

    public DigitView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DigitView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public DigitView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int defaultSide = getResources().getDimensionPixelOffset(R.dimen.digit_view_default_side);

        final int tmpW = measureHelper(widthMeasureSpec, defaultSide);
        final int tmpH = measureHelper(heightMeasureSpec, defaultSide);
        final int finalSide = Math.min(tmpW, tmpH);

        prepare(finalSide);
        setMeasuredDimension(finalSide, finalSide);
    }

    private void prepare(float finalSize) {
        final float scale = finalSize / DIGIT_SIDE;

        mEnlargeMatrix.reset();
        mNarrowMatrix.reset();

        mEnlargeMatrix.setScale(scale, scale);
        mEnlargeMatrix.invert(mNarrowMatrix);

        mRicePath.reset();
        mRicePath.moveTo(0F, 0F);
        mRicePath.lineTo(finalSize, finalSize);

        mRicePath.moveTo(0F, finalSize);
        mRicePath.lineTo(finalSize, 0F);

        final float halfSize = finalSize / 2;

        mRicePath.moveTo(0F, halfSize);
        mRicePath.lineTo(finalSize, halfSize);

        mRicePath.moveTo(halfSize, 0F);
        mRicePath.lineTo(halfSize, finalSize);

        mRicePath.close();
    }

    private int measureHelper(int spec, int defaultSide) {
        final int size = MeasureSpec.getSize(spec);
        return Math.max(defaultSide, size);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        reset();
    }

    public void reset() {
        final Rect rect = new Rect(0, 0, mDigitBitmap.getWidth(), mDigitBitmap.getHeight());
        mDigitCanvas.drawRect(rect, mBgPaint);

        postInvalidate();
    }

    public void reset(int[] pixels) {
        mDigitBitmap.setPixels(pixels, 0, mDigitBitmap.getWidth(), 0, 0, mDigitBitmap.getWidth(), mDigitBitmap.getHeight());

        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mDigitBitmap, mEnlargeMatrix, mFgPaint);
        canvas.drawPath(mRicePath, mRicePaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction() & MotionEvent.ACTION_MASK;

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                handleDownAction(event);
                return true;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_MOVE:
                handleMoveAction(event);
                return true;

            default:
                return false;
        }
    }

    private void handleMoveAction(@NonNull MotionEvent motionEvent) {
        mOldPair.copy(mNewPair);
        mNewPair.pos(motionEvent.getX(), motionEvent.getY());
        convert(mNewPair);

        drawLine(mOldPair, mNewPair);
        postInvalidate();
    }

    /**
     * In order to deeper the color of the digit
     */
    private void drawLine(@NonNull Pair oldPair, @NonNull Pair newPair) {
        for (int i = 0; i < OVERLAY_TIME; ++i) {
            mDigitCanvas.drawLine(oldPair.getX(), oldPair.getY(),
                    newPair.getX(), newPair.getY(), mFgPaint);
        }
    }

    /**
     * get darkness, 0.0 for white and 1.0 for black pixel
     */
    public io.whz.synapse.matrix.Matrix getDarkness() {
        final int side = DIGIT_SIDE;
        final int[] pixels = new int[side * side];

        mDigitBitmap.getPixels(pixels, 0, side, 0, 0, side, side);

        final double[] doubles = MNISTUtil.convertBitmap2Darkness(pixels);

        return io.whz.synapse.matrix.Matrix.array(doubles, DIGIT_SIDE * DIGIT_SIDE);
    }

    public void markTrash() {
        mIsTrash = true;
    }

    private void handleDownAction(@NonNull MotionEvent motionEvent) {
        if (mIsTrash) {
            reset();
            mIsTrash = false;
        }

        mOldPair.reset();

        mNewPair.pos(motionEvent.getX(), motionEvent.getY());
        convert(mNewPair);
    }

    private void convert(@NonNull Pair pair) {
        mNarrowMatrix.mapPoints(pair.getItems());
    }

    private static final class Pair {
        private final float[] items = new float[2];

        void pos(float x, float y) {
            items[0] = x;
            items[1] = y;
        }

        void copy(@NonNull Pair pair) {
            this.items[0] = pair.getX();
            this.items[1] = pair.getY();
        }

        void reset() {
            pos(0F, 0F);
        }

        float[] getItems() {
            return items;
        }

        float getX() {
            return this.items[0];
        }

        float getY() {
            return this.items[1];
        }
    }
} public class FigureProvider {
    private final File mFigureFile;
    private final ThreadLocalRandom mRandom;
    @Nullable private Figure[] mFigures;

    public FigureProvider(@NonNull File file) {
        Precondition.checkNotNull(file);

        mFigureFile = file;
        mRandom = ThreadLocalRandom.current();
    }

    public void load() {
        mFigures = MNISTUtil.readFigures(mFigureFile);
    }

    @Nullable
    public Figure next() {
        if (mFigures == null
                || mFigures.length == 0) {
            return null;
        }

        return mFigures[mRandom.nextInt(mFigures.length)];
    }
} public class Global {
    private final Uri mBaseDownloadUri;
    private final String[] mDataSet;

    private final Singleton<EventBus> mBus;
    private final Singleton<DaoSession> mSession;
    private final Singleton<Dir> mDirs;
    private final Singleton<SharedPreferences> mPreference;

    private Global() {
        mBaseDownloadUri = Uri.parse("http://yann.lecun.com/exdb/mnist");
        mDataSet = new String[]{
                "train-images-idx3-ubyte.gz",
                "train-labels-idx1-ubyte.gz",
                "t10k-images-idx3-ubyte.gz",
                "t10k-labels-idx1-ubyte.gz",
        };

        mBus = new Singleton<>();
        mSession = new Singleton<>();
        mDirs = new Singleton<>();
        mPreference = new Singleton<>();
    }

    public String[] getDataSet() {
        return mDataSet;
    }

    public Uri getBaseDownloadUri() {
        return mBaseDownloadUri;
    }

    public void setPreference(@NonNull SharedPreferences preference) {
        mPreference.setAndLock(Precondition.checkNotNull(preference));
    }

    public SharedPreferences getPreference() {
        return mPreference.get();
    }

    public void setBus(@NonNull EventBus bus) {
        mBus.setAndLock(Precondition.checkNotNull(bus));
    }

    public EventBus getBus() {
        return mBus.get();
    }

    public void setSession(@NonNull DaoSession session) {
        mSession.setAndLock(session);
    }

    public DaoSession getSession() {
        return mSession.get();
    }

    public void setRootDir(@NonNull File root) {
        final Dir dirs = new Dir(root);
        mDirs.setAndLock(dirs);
    }

    public Dir getDirs() {
        return mDirs.get();
    }

    public boolean isDirSet() {
        return mDirs.isSet();
    }

    public static Global getInstance() {
        return Holder.sInstance;
    }

    private interface Holder {
        Global sInstance = new Global();
    }
} public enum Scheduler implements IThreadExecutor {
    Main(new MainThread()), Secondary(new SecondaryThread());

    private final IThreadExecutor mExecutor;

    Scheduler(IThreadExecutor executor) {
        mExecutor = executor;
    }

    @Override
    public void execute(Runnable runnable) {
        mExecutor.execute(runnable);
    }

    private static class SecondaryThread implements IThreadExecutor {
        private static int sCoreThreadNum = Runtime.getRuntime().availableProcessors() + 1;
        private final ExecutorService mExecutorService = Executors.newFixedThreadPool(sCoreThreadNum);

        @Override
        public void execute(Runnable runnable) {
            mExecutorService.execute(runnable);
        }
    }

    private static class MainThread implements IThreadExecutor {
        private final Handler handler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(Runnable runnable) {
            handler.post(runnable);
        }
    }

} public class MNISTUtil {
    private static final int MASK = 0xFF;
    private static final int LABEL_MAGIC = 2049;
    private static final int IMAGE_MAGIC = 2051;
    private static final int BATCH_MAGIC = 2052;
    public static final int MAX_TRAINING_SIZE = 60000;
    public static final int PRE_FILE_SIZE = 2000;
    private static final String BATCH_FILE_SUFFIX = "batch";

    public static boolean gunzip(File sourceFile, File targetFile) {
        if (!sourceFile.exists()) {
            return false;
        }

        GZIPInputStream inputStream = null;
        FileOutputStream outputStream = null;

        boolean result;

        try {
            inputStream = new GZIPInputStream(new FileInputStream(sourceFile));
            outputStream = new FileOutputStream(targetFile);

            int len;
            final byte[] buffer = new byte[1024];

            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }

            outputStream.flush();
            result = true;
        } catch (IOException e) {
            e.printStackTrace();

            result = false;
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }

                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }

        return result;
    }

    public static List<Integer> parseLabels(File sourceFile) {
        DataInputStream inputStream = null;
        final List<Integer> list = new ArrayList<>();

        try {
            inputStream = new DataInputStream(new FileInputStream(sourceFile));

            final int magic = inputStream.readInt();

            if (magic != LABEL_MAGIC) {
                throw new IllegalArgumentException();
            }

            final int len = inputStream.readInt();
            byte cur;

            for (int i = 0; i < len; ++i) {
                cur = inputStream.readByte();
                list.add((int) cur);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }

        return list;
    }

    public static Figure[] readFigures(@NonNull File file) {
        Precondition.checkNotNull(file);

        if (!file.exists()) {
            return null;
        }

        DataInputStream inputStream = null;

        try {
            inputStream = new DataInputStream(new FileInputStream(file));

            if (inputStream.readInt() != BATCH_MAGIC) {
                return null;
            }

            final int len = inputStream.readInt();
            final int rows = inputStream.readInt();
            final int cols = inputStream.readInt();

            final Figure[] figures = new Figure[len];
            final int size = rows * cols;
            final byte[] buffer = new byte[size];
            int curNum;

            for (int i = 0; i < len; ++i) {
                curNum = inputStream.readInt();
                inputStream.read(buffer);

                figures[i] = new Figure(curNum, buffer.clone());
            }

            return figures;
        } catch (IOException e) {
            e.printStackTrace();

            return null;
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static boolean parseImages(File sourceFile, File targetDir, List<Integer> labels) {
        if (!sourceFile.exists() || !targetDir.exists()) {
            return false;
        }

        DataInputStream inputStream = null;
        DataOutputStream outputStream = null;

        boolean result;

        try {
            inputStream = new DataInputStream(new FileInputStream(sourceFile));

            final int magic = inputStream.readInt();

            if (magic != IMAGE_MAGIC) {
                throw new IllegalArgumentException();
            }

            final int len = inputStream.readInt();

            if (len != labels.size()) {
                throw new IllegalStateException();
            }

            final int rows = inputStream.readInt();
            final int cols = inputStream.readInt();
            final byte[] buffer = new byte[rows * cols];
            int count = 1;

            for (int i = 1; i <= len; ++i) {
                if (outputStream == null) {
                    outputStream = new DataOutputStream(new FileOutputStream(new File(targetDir,
                            String.format("%s.%s", count++, BATCH_FILE_SUFFIX))));
                    outputStream.writeInt(BATCH_MAGIC);
                    outputStream.writeInt(PRE_FILE_SIZE);
                    outputStream.writeInt(rows);
                    outputStream.writeInt(cols);
                }

                inputStream.read(buffer);
                outputStream.writeInt(labels.get(i - 1));
                outputStream.write(buffer);

                if (i % PRE_FILE_SIZE == 0) {
                    outputStream.flush();
                    outputStream.close();
                    outputStream = null;
                }
            }

            result = true;
        } catch (IOException e) {
            e.printStackTrace();

            result = false;
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }

                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    public static Digit[] readBatches(File file) {
        if (!file.exists()) {
            return null;
        }

        DataInputStream inputStream = null;

        try {
            inputStream = new DataInputStream(new FileInputStream(file));

            if (inputStream.readInt() != BATCH_MAGIC) {
                return null;
            }

            final int len = inputStream.readInt();
            final int rows = inputStream.readInt();
            final int cols = inputStream.readInt();

            final int size = rows * cols;
            final Digit[] digits = new Digit[len];
            final byte[] buffer = new byte[size];
            int curNum;

            for (int i = 0; i < len; ++i) {
                curNum = inputStream.readInt();
                inputStream.read(buffer);

                digits[i] = new Digit(curNum, convertMnistImage2Darkness(buffer));
            }

            return digits;
        } catch (IOException e) {
            e.printStackTrace();

            return null;
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * In Mnist, 0 represents the white color, and 255 represents the black color
     *
     */
    private static double[] convertMnistImage2Darkness(@NonNull byte[] bytes) {
        final double[] doubles = new double[bytes.length];

        for (int i = 0, len = bytes.length; i < len; ++i) {
            doubles[i] = (double) (MASK & bytes[i]) / MASK;
        }

        return doubles;
    }

    /**
     * In android bitmap, 0 represents the black color, and 255 represents the white color
     */
    public static double[] convertBitmap2Darkness(@NonNull int[] pixels) {
        final double[] res = new double[pixels.length];

        for (int i = 0, len = pixels.length; i < len; ++i) {
            res[i] = (double)(MASK - (MASK & pixels[i])) / MASK;
        }

        return res;
    }

    public static int[] convertByteArray2Bitmap(@NonNull byte[] bytes) {
        final int[] res = new int[bytes.length];
        int color;

        for (int i = 0, len = bytes.length; i < len; ++i) {
            color = MASK - (MASK & bytes[i]);
            res[i] = color << 16 | color << 8 | color | 0xFF000000;
        }

        return res;
    }
} public class NeuralNetwork {
    private static final String TAG = App.TAG + "-NeuralNetwork";
    
    public static final int INPUT_LAYER_NUMBER = 784;
    public static final int OUTPUT_LAYER_NUMBER = 10;

    private final Matrix[] mWeights;
    private final Matrix[] mBiases;

    public NeuralNetwork(@NonNull int... hiddenLayerSizes) {
        Precondition.checkNotNull(hiddenLayerSizes, "Hidden layers must not be null!");

        final int hiddenLen;
        Precondition.checkArgument((hiddenLen = hiddenLayerSizes.length) != 0, "Size of Hidden layers must not be zero!");

        final int[] totalSizes = new int[hiddenLen + 2];
        System.arraycopy(hiddenLayerSizes, 0, totalSizes, 1, hiddenLen);
        totalSizes[0] = INPUT_LAYER_NUMBER;
        totalSizes[totalSizes.length - 1] = OUTPUT_LAYER_NUMBER;

        mBiases = newBiasesMatrix(totalSizes);
        mWeights = newWeightsMatrices(totalSizes);
    }

    public NeuralNetwork(@NonNull Matrix[] weights, @NonNull Matrix[] biases) {
        Precondition.checkNotNull(weights);
        Precondition.checkNotNull(biases);

        mWeights = weights;
        mBiases = biases;
    }

    private Matrix[] newWeightsMatrices(int[] totalSize) {
        final int len = totalSize.length - 1;

        final int[] rows = new int[len];
        System.arraycopy(totalSize, 1, rows, 0, len);

        final int[] cols = new int[len];
        System.arraycopy(totalSize, 0, cols, 0, len);

        return MatrixUtil.randns(rows, cols);
    }

    private Matrix[] newBiasesMatrix(int[] totalSize) {
        final int len = totalSize.length - 1;

        final int[] rows = new int[len];
        System.arraycopy(totalSize, 1, rows, 0, rows.length);

        final int[] cols = new int[len];
        Arrays.fill(cols, 1);

        return MatrixUtil.randns(rows, cols);
    }

    public Matrix[] getBiases() {
        return mBiases;
    }

    public Matrix[] getWeights() {
        return mWeights;
    }

    public void train(int epochs, double learningRate,
                      @NonNull DataSet trainDataSet, @NotNull DataSet validateDataSet,
                      @NonNull DataSet testDataSet, @NonNull TrainCallback callback) {
        Precondition.checkArgument(epochs > 0, "Epochs must greater than 0");
        Precondition.checkArgument(learningRate > 0D, "Learning rate must greater than 0");
        Precondition.checkNotNull(trainDataSet);

        new TrainRunnable(epochs, learningRate, trainDataSet,
                validateDataSet, testDataSet, mBiases, mWeights, callback)
                .run();
    }

    private static class TrainRunnable implements Runnable {
        private final int mEpochs;
        private final double mLearningRate;
        private final DataSet mTraining;
        private final DataSet mValidation;
        private final DataSet mTest;
        private final Matrix[] mBiases;
        private final Matrix[] mWeights;
        private TrainCallback mCallback;

        private TrainRunnable(int epochs, double learningRate, DataSet training,
                              DataSet validation, DataSet test, Matrix[] biases, Matrix[] weights,
                              TrainCallback callback) {
            mEpochs = epochs;
            mLearningRate = learningRate;
            mTraining = training;
            mValidation = validation;
            mTest = test;
            mBiases = biases;
            mWeights = weights;
            mCallback = callback;
        }

        @Override
        public void run() {
            mCallback.onStart();

            for (int i = 1; i <= mEpochs; ++i) {
                mTraining.shuffle();
                sgd();

                final double rate = evaluate(mValidation);

                if (!mCallback.onUpdate(i, rate)) {
                    mCallback.onEvaluate();
                    return;
                }
            }

            mCallback.onEvaluate();

            final double rate = evaluate(mTest);
            mCallback.onComplete(rate);
        }

        private void sgd() {
            Batch batch;

            while ((batch = mTraining.nextBatch()) != null) {
                updateMiniBatch(batch.inputs, batch.targets);
            }
        }

        private void updateMiniBatch(Matrix[] inputs, Matrix[] targets) {
            final Matrix[] batchWeights = MatrixUtil.zerosLike(mWeights);
            final Matrix[] batchBiases = MatrixUtil.zerosLike(mBiases);
            final int len = inputs.length;

            for (int i = 0; i < len; ++i) {
                feed(inputs[i], targets[i], batchWeights, batchBiases);
            }

            update(batchWeights, batchBiases, len);
        }

        private void update(Matrix[] batchWeights, Matrix[] batchBiases, int count) {
            final int len = mWeights.length;
            final double tmp = mLearningRate / count;

            for (int i = 0; i < len; ++i) {
                mWeights[i].minusTo(batchWeights[i].times(tmp));
                mBiases[i].minusTo(batchBiases[i].times(tmp));
            }
        }

        private void feed(Matrix input, Matrix target, Matrix[] batchWeights, Matrix[] batchBiases) {
            final Matrix[] activations = forwardPropagation(input);
            final int aLen = activations.length;
            final int bLen = batchWeights.length;

            final Matrix error = activations[aLen - 1].minus(target);
            Matrix delta = error.times(ActivateFunction.sigmoidPrime(activations[aLen - 1]));

            batchBiases[bLen - 1].plusTo(delta);
            batchWeights[bLen - 1].plusTo(delta.dot(activations[aLen - 2].transpose()));

            for (int i = 2; i < aLen; ++i) {
                final Matrix prime = ActivateFunction.sigmoidPrime(activations[aLen - i]);
                delta = mWeights[bLen - i + 1].transpose()
                        .dot(delta)
                        .times(prime);

                batchBiases[bLen - i].plusTo(delta);
                batchWeights[bLen - i].plusTo(delta.dot(activations[aLen - i - 1].transpose()));
            }
        }

        private Matrix[] forwardPropagation(@NonNull Matrix input) {
            final int len = mWeights.length;
            final Matrix[] activations = new Matrix[len + 1];
            activations[0] = input;

            for (int i = 0; i < len; ++i) {
                final Matrix matrix = mWeights[i]
                        .dot(activations[i])
                        .plus(mBiases[i]);

                activations[i + 1] = ActivateFunction.sigmoid(matrix);
            }

            return activations;
        }

        private double evaluate(@NonNull DataSet dataSet) {
            Batch batch;
            int correct = 0;
            int total = 0;

            dataSet.shuffle();

            while ((batch = dataSet.nextBatch()) != null) {
                final Matrix[] inputs = batch.inputs;
                final Matrix[] targets = batch.targets;
                final int len = inputs.length;
                total += len;

                for (int i = 0; i < len; ++i) {
                    final Matrix output = feedForward(mWeights, mBiases, inputs[i]);

                    if (MatrixUtil.argmax(output) == MatrixUtil.index(targets[i])) {
                        ++correct;
                    }
                }
            }

            return total != 0 ? (double) correct / total : 0D;
        }
    }

    public Pair<Integer, Double> predict(@NonNull Matrix input) {
        return MatrixUtil.findMax(feedForward(mWeights, mBiases, input));
    }

    private static Matrix feedForward(@NonNull Matrix[] weights, @NonNull Matrix[] biases,
                                      @NonNull Matrix input) {
        final int len = weights.length;
        Matrix res = input;

        for (int i = 0; i < len; ++i) {
            res = ActivateFunction.sigmoid(weights[i].dot(res).plus(biases[i]));
        }

        return res;
    }
} public interface PreferenceCons {
    String IS_DATA_SET_READY = "is_downloaded_data_set";
    String IS_FIST_PLAY = "is_fist_play";
    String SHOULD_ADD_DEMO_MODEL = "should_add_demo_model";
} public class TrackCons {
    private static final String SPLIT = "_";
    private static final String INDEX = BuildConfig.DEBUG ? "debug" : "";

    public interface Main {
        String INDEX = concat(TrackCons.INDEX, MainActivity.class.getSimpleName().toLowerCase());

        String CLICK_DOWNLOAD = concat(INDEX, "click_download");
        String CLICK_FAB = concat(INDEX, "click_fab");
        String CLICK_PLAY = concat(INDEX, "click_play");
        String CLICK_TRAINED = concat(INDEX, "click_trained");
        String CLICK_TRAINING = concat(INDEX, "click_training");
        String CLICK_ABOUT = concat(INDEX, "click_about");

        String SCROLL_BOTTOM = concat(INDEX, "scroll_bottom");
    }

    public interface Play {
        String INDEX = concat(TrackCons.INDEX, PlayActivity.class.getSimpleName().toLowerCase());

        String CLICK_MODEL_SELECTION = concat(INDEX, "click_model_selection");
        String CLICK_VIEW_DETAIL = concat(INDEX, "click_view_detail");

        String PLAY_MNIST = concat(INDEX, "play_mnist");
        String PLAY_HAND_WRITE = concat(INDEX, "play_hand_write");
    }

    public interface Model {
        String INDEX = concat(TrackCons.INDEX, NeuralModelActivity.class.getSimpleName().toLowerCase());

        String CLICK_ADD_NEW_LAYER = concat(INDEX, "click_add_new_layer");
        String CLICK_LAYER_DELETE = concat(INDEX, "click_layer_delete");
        String CLICK_TRAIN = concat(INDEX, "click_train");
    }

    public interface Detail {
        String INDEX = concat(TrackCons.INDEX, ModelDetailActivity.class.getSimpleName().toLowerCase());

        String CLICK_INTERRUPT = concat(INDEX, "click_interrupt");
        String CLICK_DELETE = concat(INDEX, "click_delete");
        String CLICK_PLAY = concat(INDEX, "click_play");
    }

    public interface About {
        String INDEX = concat(TrackCons.INDEX, AboutDialog.class.getSimpleName().toLowerCase());

        String CLICK_GITHUB = concat(INDEX, "click_github");
        String CLICK_RATE = concat(INDEX, "click_rate");
        String CLICK_SHARE = concat(INDEX, "click_share");
    }

    public interface Service {
        String INDEX = concat(TrackCons.INDEX, MainService.class.getSimpleName().toLowerCase());

        String DOWNLOAD = concat(INDEX, "download");
        String DECOMPRESS = concat(INDEX, "decompress");
        String TRAIN = concat(INDEX, "train");
        String INTERRUPT_REQ = concat(INDEX, "interrupt_req");
        String INTERRUPT = concat(INDEX, "interrupt");
    }

    public interface APP {
        String INDEX = concat(TrackCons.INDEX, App.class.getSimpleName().toLowerCase());

        String INITIALIZE = concat(INDEX, "initialize");
        String CAUGHT = concat(INDEX, "catch_exception");
    }

    public interface Key {
        String SUCCESS = "success";
        String MSG = "msg";
        String TIME_USED = "time_used";
    }

    public interface Lifecycle {
        String INDEX = concat(TrackCons.INDEX, "lifecycle");

        String ENTER = concat(INDEX, "enter");
        String LEAVE = concat(INDEX, "leave");
    }

    public static String concat(@NonNull String... array) {
        final StringBuilder builder = new StringBuilder();

        for (int i = 0, len = array.length; i < len; ++i) {
            if (builder.length() != 0) {
                builder.append(SPLIT);
            }

            builder.append(array[i]);
        }

        return builder.toString();
    }
} @Entity
public class DBModel {
    @Id(autoincrement = true)
    private Long id;

    @Unique
    private String name;
    private long createdTime;
    private double learningRate;
    private int epochs;
    private int dataSize;
    private long timeUsed;
    private double evaluate;
    private byte[] hiddenSizeBytes;
    private byte[] accuracyBytes;
    private byte[] biasBytes;
    private byte[] weightBytes;
    @Generated(hash = 156277296)
    public DBModel(Long id, String name, long createdTime, double learningRate,
            int epochs, int dataSize, long timeUsed, double evaluate,
            byte[] hiddenSizeBytes, byte[] accuracyBytes, byte[] biasBytes,
            byte[] weightBytes) {
        this.id = id;
        this.name = name;
        this.createdTime = createdTime;
        this.learningRate = learningRate;
        this.epochs = epochs;
        this.dataSize = dataSize;
        this.timeUsed = timeUsed;
        this.evaluate = evaluate;
        this.hiddenSizeBytes = hiddenSizeBytes;
        this.accuracyBytes = accuracyBytes;
        this.biasBytes = biasBytes;
        this.weightBytes = weightBytes;
    }
    @Generated(hash = 1265045159)
    public DBModel() {
    }
    public Long getId() {
        return this.id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public long getCreatedTime() {
        return this.createdTime;
    }
    public void setCreatedTime(long createdTime) {
        this.createdTime = createdTime;
    }
    public double getLearningRate() {
        return this.learningRate;
    }
    public void setLearningRate(double learningRate) {
        this.learningRate = learningRate;
    }
    public int getEpochs() {
        return this.epochs;
    }
    public void setEpochs(int epochs) {
        this.epochs = epochs;
    }
    public int getDataSize() {
        return this.dataSize;
    }
    public void setDataSize(int dataSize) {
        this.dataSize = dataSize;
    }
    public long getTimeUsed() {
        return this.timeUsed;
    }
    public void setTimeUsed(long timeUsed) {
        this.timeUsed = timeUsed;
    }
    public double getEvaluate() {
        return this.evaluate;
    }
    public void setEvaluate(double evaluate) {
        this.evaluate = evaluate;
    }
    public byte[] getHiddenSizeBytes() {
        return this.hiddenSizeBytes;
    }
    public void setHiddenSizeBytes(byte[] hiddenSizeBytes) {
        this.hiddenSizeBytes = hiddenSizeBytes;
    }
    public byte[] getAccuracyBytes() {
        return this.accuracyBytes;
    }
    public void setAccuracyBytes(byte[] accuracyBytes) {
        this.accuracyBytes = accuracyBytes;
    }
    public byte[] getBiasBytes() {
        return this.biasBytes;
    }
    public void setBiasBytes(byte[] biasBytes) {
        this.biasBytes = biasBytes;
    }
    public byte[] getWeightBytes() {
        return this.weightBytes;
    }
    public void setWeightBytes(byte[] weightBytes) {
        this.weightBytes = weightBytes;
    }
} public class Model implements Serializable {
    public static final long serialVersionUID = 0xAAFF;

    private Long id;
    private String name;
    private long createdTime;
    private double learningRate;
    private int epochs;
    private int dataSize;
    private long timeUsed;
    private double evaluate;
    private int stepEpoch;
    private double[] accuracies;
    private int[] hiddenSizes;
    private Matrix[] biases;
    private Matrix[] weights;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public long getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(long createdTime) {
        this.createdTime = createdTime;
    }

    public double getLearningRate() {
        return learningRate;
    }

    public void setLearningRate(double learningRate) {
        this.learningRate = learningRate;
    }

    public int getEpochs() {
        return epochs;
    }

    public void setEpochs(int epochs) {
        this.epochs = epochs;
    }

    public int getDataSize() {
        return dataSize;
    }

    public void setDataSize(int dataSize) {
        this.dataSize = dataSize;
    }

    public long getTimeUsed() {
        return timeUsed;
    }

    public void setTimeUsed(long timeUsed) {
        this.timeUsed = timeUsed;
    }

    public double getEvaluate() {
        return evaluate;
    }

    public void setEvaluate(double evaluate) {
        this.evaluate = evaluate;
    }

    public int getStepEpoch() {
        return stepEpoch;
    }

    public void setStepEpoch(int stepEpoch) {
        this.stepEpoch = stepEpoch;
    }

    public int[] getHiddenSizes() {
        return hiddenSizes;
    }

    public void setHiddenSizes(int[] hiddenSizes) {
        this.hiddenSizes = hiddenSizes;
    }

    public Matrix[] getBiases() {
        return biases;
    }

    public void setBiases(Matrix[] biases) {
        this.biases = biases;
    }

    public Matrix[] getWeights() {
        return weights;
    }

    public void setWeights(Matrix[] weights) {
        this.weights = weights;
    }

    public double[] getAccuracies() {
        return accuracies;
    }

    public void setAccuracies(double[] accuracies) {
        this.accuracies = accuracies;
    }
} public class TrainedModelViewBinder extends ItemViewBinder<TrainedModelItem, TrainedModelViewBinder.TrainedModelViewHolder>
        implements View.OnClickListener {
    public static final int[] FG = new int[]{
            R.color.item_chart_fg$1,
            R.color.item_chart_fg$2,
            R.color.item_chart_fg$3,
            R.color.item_chart_fg$4,
            R.color.item_chart_fg$5,
            R.color.item_chart_fg$6,
            R.color.item_chart_fg$7,
            R.color.item_chart_fg$8,
            R.color.item_chart_fg$9,
            R.color.item_chart_fg$10
    };

    public static final int[] BG = new int[]{
            R.color.item_chart_bg$1,
            R.color.item_chart_bg$2,
            R.color.item_chart_bg$3,
            R.color.item_chart_bg$4,
            R.color.item_chart_bg$5,
            R.color.item_chart_bg$6,
            R.color.item_chart_bg$7,
            R.color.item_chart_bg$8,
            R.color.item_chart_bg$9,
            R.color.item_chart_bg$10
    };

    @NonNull
    @Override
    protected TrainedModelViewHolder onCreateViewHolder(@NonNull LayoutInflater layoutInflater, @NonNull ViewGroup viewGroup) {
        final TrainedModelViewHolder holder = TrainedModelViewHolder.newInstance(layoutInflater, viewGroup);

        holder.itemView.setOnClickListener(this);
        prepareChart(holder.chart);

        return holder;
    }

    @Override
    public void onClick(View view) {
        final Long id = (Long) view.getTag();

        if (id == null) {
            return;
        }

        Global.getInstance()
                .getBus()
                .post(new MANEvent<>(MANEvent.JUMP_TO_TRAINED, id));
    }

    private void prepareChart(@NonNull LineChart chart) {
        chart.getDescription().setEnabled(false);
        chart.setTouchEnabled(false);
        chart.setDragEnabled(false);
        chart.setScaleEnabled(false);
        chart.setHighlightPerDragEnabled(false);
        chart.setPinchZoom(false);
        chart.setDrawGridBackground(true);
        chart.getLegend().setEnabled(false);
        chart.getAxisRight().setEnabled(false);
        chart.getAxisLeft().setEnabled(false);
        chart.getXAxis().setEnabled(false);
        chart.setViewPortOffsets(0, 0, 0, 0);
    }

    @Override
    protected void onBindViewHolder(@NonNull TrainedModelViewHolder holder, @NonNull TrainedModelItem item) {
        if (holder.data == null) {
            holder.data = prepareInitData(holder.chart, item.getEntries());
        } else {
            holder.data.setValues(item.getEntries());
            holder.chart.getData().notifyDataChanged();
            holder.chart.notifyDataSetChanged();
        }

        holder.itemView.setTag(item.getModel().getId());
        renderModel(holder, item.getModel());
        changeStyle(item.getModel().getId(), holder.chart, holder.data);

        holder.chart.invalidate();
    }

    private void changeStyle(long id, LineChart chart, LineDataSet set) {
        final int index = (int) (id % FG.length);
        final Context context = chart.getContext();

        final int fg = ContextCompat.getColor(context, FG[index]);
        set.setColor(fg);
        set.setFillColor(fg);

        chart.setGridBackgroundColor(ContextCompat.getColor(context, BG[index]));
    }

    private LineDataSet prepareInitData(@NonNull LineChart chart, @NonNull List<Entry> entries) {
        final LineDataSet set = new LineDataSet(entries, "Accuracy");

        set.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
        set.setAxisDependency(YAxis.AxisDependency.LEFT);
        set.setLineWidth(2F);
        set.setDrawCircleHole(false);
        set.setDrawCircles(false);
        set.setHighlightEnabled(false);
        set.setDrawFilled(true);

        final LineData group = new LineData(set);
        group.setDrawValues(false);

        chart.setData(group);

        return set;
    }

    private void renderModel(TrainedModelViewHolder holder, Model model) {
        holder.name.setText(model.getName());
        holder.layers.setText(StringFormatUtil.formatLayerSizes(model.getHiddenSizes()));
        holder.epochs.setText(String.format("E: %s", model.getEpochs()));
        holder.learningRate.setText(String.format("R: %s", model.getLearningRate()));
        holder.dataSize.setText(String.format("D: %s", model.getDataSize()));
        holder.timeUsed.setText(String.format("T: %s", StringFormatUtil.formatTimeUsed(model.getDataSize())));
        holder.evaluate.setText(String.format("%s%%", (int)(model.getEvaluate() * 100)));
    }

    static class TrainedModelViewHolder extends RecyclerView.ViewHolder {
        final LineChart chart;
        final TextView name;
        final TextView layers;
        final TextView epochs;
        final TextView learningRate;
        final TextView dataSize;
        final TextView timeUsed;
        final TextView evaluate;

        @Nullable LineDataSet data;

        TrainedModelViewHolder(View itemView) {
            super(itemView);

            chart = itemView.findViewById(R.id.line_chart);
            name = itemView.findViewById(R.id.name);
            layers = itemView.findViewById(R.id.layers);
            epochs = itemView.findViewById(R.id.epochs);
            learningRate = itemView.findViewById(R.id.learning_rate);
            dataSize = itemView.findViewById(R.id.data_size);
            timeUsed = itemView.findViewById(R.id.time_used);
            evaluate = itemView.findViewById(R.id.accuracy);
        }

        private static TrainedModelViewHolder newInstance(@NonNull LayoutInflater layoutInflater,
                                                          @NonNull ViewGroup viewGroup) {
            final View view = layoutInflater.inflate(R.layout.item_trained, viewGroup, false);

            return new TrainedModelViewHolder(view);
        }
    }
} public class Figure {
    public final byte[] bytes;
    public final int label;

    public Figure(int label, @NonNull byte[] bytes) {
        this.bytes = bytes;
        this.label = label;
    }
} public class ExceptionHelper {
    private static final boolean sEnable = BuildConfig.TRACK_ENABLE;
    private final Tracker mTracker;

    private ExceptionHelper() {
        mTracker = Tracker.getInstance();
    }

    public void caught(@NotNull Throwable e) {
        if (sEnable) {
            mTracker.event(CAUGHT)
                    .put(MSG, toString(e))
                    .log();
        } else {
            e.printStackTrace();
        }
    }

    private String toString(@NotNull Throwable e) {
        final StackTraceElement[] elements = e.getStackTrace();

        if (elements == null
                || elements.length == 0) {
            return "";
        }

        final StringBuilder builder = new StringBuilder();

        for (StackTraceElement element : elements) {
            builder.append(element.toString())
                    .append("\n");
        }

        return builder.toString();
    }

    public static ExceptionHelper getInstance() {
        return Holder.sInstance;
    }

    private interface Holder {
        ExceptionHelper sInstance = new ExceptionHelper();
    }
} public class Tracker implements ITracker {
    private final Set<AbsTrackHandler> mTracks = new HashSet<>();

    private EventBus mBus;

    public void initialize(@NonNull Context context, @NonNull EventBus bus) {
        mBus = Precondition.checkNotNull(bus);

        if (BuildConfig.TRACK_ENABLE) {
            mTracks.add(new FirebaseTrackHandler(context));
            mTracks.add(new AmplitudeTrackHandler(context));
        } else {
            mTracks.add(new DebugTrackHandler(context));
        }

        for (AbsTrackHandler track : mTracks) {
            track.register(bus);
        }

        if (context instanceof Application) {
            ((Application) context).registerActivityLifecycleCallbacks(new ActivityLifecycleTracker(this));
        }
    }

    @Override
    public void logEvent(@NonNull TrackEvent event) {
        if (mBus == null) {
            new NullPointerException("EvenBus is null, please initialize first")
                    .printStackTrace();
            return;
        }

        mBus.post(event);
    }

    public void logEvent(@NonNull String id) {
        Precondition.checkNotNull(id);

        logEvent(new TrackEvent(id, null));
    }

    public EventBuilder event(@NonNull String id) {
        Precondition.checkNotNull(id);

        return new EventBuilder(id);
    }

    public static Tracker getInstance() {
        return Holder.sInstance;
    }

    private interface Holder {
        Tracker sInstance = new Tracker();
    }

    public static class EventBuilder{
        private final Bundle bundle = new Bundle();
        private final String id;

        EventBuilder(@NonNull String id) {
            this.id = id;
        }

        public EventBuilder put(@NonNull String key, boolean value) {
            bundle.putBoolean(key, value);

            return this;
        }

        public EventBuilder put(@NonNull String key, int value) {
            bundle.putInt(key, value);

            return this;
        }

        public EventBuilder put(@NonNull String key, double value) {
            bundle.putDouble(key, value);
            return this;
        }

        public EventBuilder put(@NonNull String key, String value) {
            bundle.putString(key, value);

            return this;
        }

        public EventBuilder put(@NonNull String key, Serializable value) {
            bundle.putSerializable(key, value);

            return this;
        }

        public void log() {
            Holder.sInstance.logEvent(new TrackEvent(id, bundle));
        }
    }
} public class DbHelper {
    public static Model dbModel2Model(@NonNull DBModel dbModel) {
        Precondition.checkNotNull(dbModel);

        final Model model = new Model();

        model.setId(dbModel.getId());
        model.setName(dbModel.getName());
        model.setCreatedTime(dbModel.getCreatedTime());
        model.setLearningRate(dbModel.getLearningRate());
        model.setEpochs(dbModel.getEpochs());
        model.setStepEpoch(dbModel.getEpochs());
        model.setDataSize(dbModel.getDataSize());
        model.setTimeUsed(dbModel.getTimeUsed());
        model.setEvaluate(dbModel.getEvaluate());
        model.setHiddenSizes(byteArray2IntArray(dbModel.getHiddenSizeBytes()));
        model.setAccuracies(byteArray2DoubleArray(dbModel.getAccuracyBytes()));
        model.setBiases(byteArray2MatrixArray(dbModel.getBiasBytes()));
        model.setWeights(byteArray2MatrixArray(dbModel.getWeightBytes()));

        return model;
    }

    public static DBModel model2DBModel(@NonNull Model model) {
        Precondition.checkNotNull(model);

        final DBModel dbModel = new DBModel();

        dbModel.setId(model.getId());
        dbModel.setName(model.getName());
        dbModel.setCreatedTime(model.getCreatedTime());
        dbModel.setLearningRate(model.getLearningRate());
        dbModel.setEpochs(model.getEpochs());
        dbModel.setDataSize(model.getDataSize());
        dbModel.setTimeUsed(model.getTimeUsed());
        dbModel.setEvaluate(model.getEvaluate());
        dbModel.setHiddenSizeBytes(convert2ByteArray(model.getHiddenSizes()));
        dbModel.setAccuracyBytes(convert2ByteArray(model.getAccuracies()));
        dbModel.setBiasBytes(convert2ByteArray(model.getBiases()));
        dbModel.setWeightBytes(convert2ByteArray(model.getWeights()));

        return dbModel;
    }

    @Nullable
    private static byte[] convert2ByteArray(int... array) {
        if (array == null) {
            return null;
        }

        ByteBuffer buffer = null;

        try {
            buffer = ByteBuffer.allocate(array.length << 2);

            for (int i : array) {
                buffer.putInt(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
            buffer = null;
        }

        return buffer == null ? null : buffer.array();
    }

    @Nullable
    private static byte[] convert2ByteArray(double... array) {
        if (array == null) {
            return  null;
        }

        ByteBuffer buffer = null;

        try {
            buffer = ByteBuffer.allocate(array.length << 3);

            for (double i : array) {
                buffer.putDouble(i);
            }
        } catch (Exception e) {
            e.printStackTrace();

            buffer = null;
        }

        return buffer == null ? null : buffer.array();
    }

    @Nullable
    private static byte[] convert2ByteArray(Matrix... matrices) {
        if (matrices == null) {
            return null;
        }

        ByteBuffer buffer = null;
        int sum = 0;

        sum += 4;

        for (Matrix matrix : matrices) {
            sum += calMatrixLen(matrix);
        }

        try {
            buffer = ByteBuffer.allocate(sum);

            buffer.putInt(matrices.length);

            for (Matrix matrix : matrices) {
                buffer.putInt(matrix.getRow());
                buffer.putInt(matrix.getCol());

                final double[] doubles = matrix.getArray();

                for (double d : doubles) {
                    buffer.putDouble(d);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return buffer == null ? null : buffer.array();
    }

    private static int calMatrixLen(@NonNull Matrix matrix) {
        int sum = 0;

        sum += 8;
        sum += (matrix.getArray().length << 3);

        return sum;
    }

    @Nullable
    private static Matrix[] byteArray2MatrixArray(byte... array) {
        if (array == null) {
            return null;
        }

        Matrix[] res = null;

        try {
            final ByteBuffer buffer = ByteBuffer.wrap(array);

            final int len = buffer.getInt();
            res = new Matrix[len];

            for (int i = 0; i < len; ++i) {
                final int row = buffer.getInt();
                final int col = buffer.getInt();
                final double[] doubles = new double[row * col];

                for (int j = 0, jLen = doubles.length; j < jLen; ++j) {
                    doubles[j] = buffer.getDouble();
                }

                res[i] = Matrix.array(doubles, row);
            }
        } catch (Exception e) {
            e.printStackTrace();

            res = null;
        }

        return res;
    }

    @Nullable
    private static int[] byteArray2IntArray(byte... array) {
        if (array == null) {
            return null;
        }

        final int[] res = new int[array.length >> 2];

        try {
            final IntBuffer buffer = ByteBuffer.wrap(array).asIntBuffer();

            for (int i = 0, iLen = res.length; i < iLen; ++i) {
                res[i] = buffer.get();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return res;
    }

    @Nullable
    private static double[] byteArray2DoubleArray(byte... array) {
        if (array == null) {
            return null;
        }

        final int len = array.length;
        final double[] res = new double[len >> 3];

        try {
            final DoubleBuffer buffer = ByteBuffer.wrap(array).asDoubleBuffer();

            for (int i = 0, iLen = len >> 3; i < iLen; ++i) {
                res[i] = buffer.get();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return res;
    }
} @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class Precondition {
    public static void checkArgument(boolean expression) {
        if (!expression) {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Ensures that an expression checking an argument is true.
     *
     * @param expression the expression to check
     * @param errorMessage the exception message to use if the check fails; will
     *     be converted to a string using {@link String#valueOf(Object)}
     * @throws IllegalArgumentException if {@code expression} is false
     */
    public static void checkArgument(boolean expression, final Object errorMessage) {
        if (!expression) {
            throw new IllegalArgumentException(String.valueOf(errorMessage));
        }
    }

    /**
     * Ensures that an string reference passed as a parameter to the calling
     * method is not empty.
     *
     * @param string an string reference
     * @return the string reference that was validated
     * @throws IllegalArgumentException if {@code string} is empty
     */
    public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string) {
        if (TextUtils.isEmpty(string)) {
            throw new IllegalArgumentException();
        }
        return string;
    }

    /**
     * Ensures that an string reference passed as a parameter to the calling
     * method is not empty.
     *
     * @param string an string reference
     * @param errorMessage the exception message to use if the check fails; will
     *     be converted to a string using {@link String#valueOf(Object)}
     * @return the string reference that was validated
     * @throws IllegalArgumentException if {@code string} is empty
     */
    public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string,
            final Object errorMessage) {
        if (TextUtils.isEmpty(string)) {
            throw new IllegalArgumentException(String.valueOf(errorMessage));
        }
        return string;
    }

    /**
     * Ensures that an object reference passed as a parameter to the calling
     * method is not null.
     *
     * @param reference an object reference
     * @return the non-null reference that was validated
     * @throws NullPointerException if {@code reference} is null
     */
    public static @NonNull <T> T checkNotNull(final T reference) {
        if (reference == null) {
            throw new NullPointerException();
        }
        return reference;
    }

    /**
     * Ensures that an object reference passed as a parameter to the calling
     * method is not null.
     *
     * @param reference an object reference
     * @param errorMessage the exception message to use if the check fails; will
     *     be converted to a string using {@link String#valueOf(Object)}
     * @return the non-null reference that was validated
     * @throws NullPointerException if {@code reference} is null
     */
    public static @NonNull <T> T checkNotNull(final T reference, final Object errorMessage) {
        if (reference == null) {
            throw new NullPointerException(String.valueOf(errorMessage));
        }
        return reference;
    }

    /**
     * Ensures the truth of an expression involving the state of the calling
     * instance, but not involving any parameters to the calling method.
     *
     * @param expression a boolean expression
     * @param message exception message
     * @throws IllegalStateException if {@code expression} is false
     */
    public static void checkState(final boolean expression, String message) {
        if (!expression) {
            throw new IllegalStateException(message);
        }
    }

    /**
     * Ensures the truth of an expression involving the state of the calling
     * instance, but not involving any parameters to the calling method.
     *
     * @param expression a boolean expression
     * @throws IllegalStateException if {@code expression} is false
     */
    public static void checkState(final boolean expression) {
        checkState(expression, null);
    }

    /**
     * Check the requested flags, throwing if any requested flags are outside
     * the allowed set.
     *
     * @return the validated requested flags.
     */
    public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
        if ((requestedFlags & allowedFlags) != requestedFlags) {
            throw new IllegalArgumentException("Requested flags 0x"
                    + Integer.toHexString(requestedFlags) + ", but only 0x"
                    + Integer.toHexString(allowedFlags) + " are allowed");
        }

        return requestedFlags;
    }

    /**
     * Ensures that that the argument numeric value is non-negative.
     *
     * @param value a numeric int value
     * @param errorMessage the exception message to use if the check fails
     * @return the validated numeric value
     * @throws IllegalArgumentException if {@code value} was negative
     */
    public static @IntRange(from = 0) int checkArgumentNonnegative(final int value,
            final String errorMessage) {
        if (value < 0) {
            throw new IllegalArgumentException(errorMessage);
        }

        return value;
    }

    /**
     * Ensures that that the argument numeric value is non-negative.
     *
     * @param value a numeric int value
     *
     * @return the validated numeric value
     * @throws IllegalArgumentException if {@code value} was negative
     */
    public static @IntRange(from = 0) int checkArgumentNonnegative(final int value) {
        if (value < 0) {
            throw new IllegalArgumentException();
        }

        return value;
    }

    /**
     * Ensures that that the argument numeric value is non-negative.
     *
     * @param value a numeric long value
     * @return the validated numeric value
     * @throws IllegalArgumentException if {@code value} was negative
     */
    public static long checkArgumentNonnegative(final long value) {
        if (value < 0) {
            throw new IllegalArgumentException();
        }

        return value;
    }

    /**
     * Ensures that that the argument numeric value is non-negative.
     *
     * @param value a numeric long value
     * @param errorMessage the exception message to use if the check fails
     * @return the validated numeric value
     * @throws IllegalArgumentException if {@code value} was negative
     */
    public static long checkArgumentNonnegative(final long value, final String errorMessage) {
        if (value < 0) {
            throw new IllegalArgumentException(errorMessage);
        }

        return value;
    }

    /**
     * Ensures that that the argument numeric value is positive.
     *
     * @param value a numeric int value
     * @param errorMessage the exception message to use if the check fails
     * @return the validated numeric value
     * @throws IllegalArgumentException if {@code value} was not positive
     */
    public static int checkArgumentPositive(final int value, final String errorMessage) {
        if (value <= 0) {
            throw new IllegalArgumentException(errorMessage);
        }

        return value;
    }

    /**
     * Ensures that the argument floating point value is a finite number.
     *
     * <p>A finite number is defined to be both representable (that is, not NaN) and
     * not infinite (that is neither positive or negative infinity).</p>
     *
     * @param value a floating point value
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated floating point value
     *
     * @throws IllegalArgumentException if {@code value} was not finite
     */
    public static float checkArgumentFinite(final float value, final String valueName) {
        if (Float.isNaN(value)) {
            throw new IllegalArgumentException(valueName + " must not be NaN");
        } else if (Float.isInfinite(value)) {
            throw new IllegalArgumentException(valueName + " must not be infinite");
        }

        return value;
    }

    /**
     * Ensures that the argument floating point value is within the inclusive range.
     *
     * <p>While this can be used to range check against +/- infinity, note that all NaN numbers
     * will always be out of range.</p>
     *
     * @param value a floating point value
     * @param lower the lower endpoint of the inclusive range
     * @param upper the upper endpoint of the inclusive range
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated floating point value
     *
     * @throws IllegalArgumentException if {@code value} was not within the range
     */
    public static float checkArgumentInRange(float value, float lower, float upper,
            String valueName) {
        if (Float.isNaN(value)) {
            throw new IllegalArgumentException(valueName + " must not be NaN");
        } else if (value < lower) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%f, %f] (too low)", valueName, lower, upper));
        } else if (value > upper) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%f, %f] (too high)", valueName, lower, upper));
        }

        return value;
    }

    /**
     * Ensures that the argument int value is within the inclusive range.
     *
     * @param value a int value
     * @param lower the lower endpoint of the inclusive range
     * @param upper the upper endpoint of the inclusive range
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated int value
     *
     * @throws IllegalArgumentException if {@code value} was not within the range
     */
    public static int checkArgumentInRange(int value, int lower, int upper,
            String valueName) {
        if (value < lower) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
        } else if (value > upper) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
        }

        return value;
    }

    /**
     * Ensures that the argument long value is within the inclusive range.
     *
     * @param value a long value
     * @param lower the lower endpoint of the inclusive range
     * @param upper the upper endpoint of the inclusive range
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated long value
     *
     * @throws IllegalArgumentException if {@code value} was not within the range
     */
    public static long checkArgumentInRange(long value, long lower, long upper,
            String valueName) {
        if (value < lower) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
        } else if (value > upper) {
            throw new IllegalArgumentException(
                    String.format(
                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
        }

        return value;
    }

    /**
     * Ensures that the array is not {@code null}, and none of its elements are {@code null}.
     *
     * @param value an array of boxed objects
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated array
     *
     * @throws NullPointerException if the {@code value} or any of its elements were {@code null}
     */
    public static <T> T[] checkArrayElementsNotNull(final T[] value, final String valueName) {
        if (value == null) {
            throw new NullPointerException(valueName + " must not be null");
        }

        for (int i = 0; i < value.length; ++i) {
            if (value[i] == null) {
                throw new NullPointerException(
                        String.format("%s[%d] must not be null", valueName, i));
            }
        }

        return value;
    }

    /**
     * Ensures that the {@link Collection} is not {@code null}, and none of its elements are
     * {@code null}.
     *
     * @param value a {@link Collection} of boxed objects
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated {@link Collection}
     *
     * @throws NullPointerException if the {@code value} or any of its elements were {@code null}
     */
    public static @NonNull <C extends Collection<T>, T> C checkCollectionElementsNotNull(
            final C value, final String valueName) {
        if (value == null) {
            throw new NullPointerException(valueName + " must not be null");
        }

        long ctr = 0;
        for (T elem : value) {
            if (elem == null) {
                throw new NullPointerException(
                        String.format("%s[%d] must not be null", valueName, ctr));
            }
            ++ctr;
        }

        return value;
    }

    /**
     * Ensures that the {@link Collection} is not {@code null}, and contains at least one element.
     *
     * @param value a {@link Collection} of boxed elements.
     * @param valueName the name of the argument to use if the check fails.

     * @return the validated {@link Collection}
     *
     * @throws NullPointerException if the {@code value} was {@code null}
     * @throws IllegalArgumentException if the {@code value} was empty
     */
    public static <T> Collection<T> checkCollectionNotEmpty(final Collection<T> value,
            final String valueName) {
        if (value == null) {
            throw new NullPointerException(valueName + " must not be null");
        }
        if (value.isEmpty()) {
            throw new IllegalArgumentException(valueName + " is empty");
        }
        return value;
    }

    /**
     * Ensures that all elements in the argument floating point array are within the inclusive range
     *
     * <p>While this can be used to range check against +/- infinity, note that all NaN numbers
     * will always be out of range.</p>
     *
     * @param value a floating point array of values
     * @param lower the lower endpoint of the inclusive range
     * @param upper the upper endpoint of the inclusive range
     * @param valueName the name of the argument to use if the check fails
     *
     * @return the validated floating point value
     *
     * @throws IllegalArgumentException if any of the elements in {@code value} were out of range
     * @throws NullPointerException if the {@code value} was {@code null}
     */
    public static float[] checkArrayElementsInRange(float[] value, float lower, float upper,
            String valueName) {
        checkNotNull(value, valueName + " must not be null");

        for (int i = 0; i < value.length; ++i) {
            float v = value[i];

            if (Float.isNaN(v)) {
                throw new IllegalArgumentException(valueName + "[" + i + "] must not be NaN");
            } else if (v < lower) {
                throw new IllegalArgumentException(
                        String.format("%s[%d] is out of range of [%f, %f] (too low)",
                                valueName, i, lower, upper));
            } else if (v > upper) {
                throw new IllegalArgumentException(
                        String.format("%s[%d] is out of range of [%f, %f] (too high)",
                                valueName, i, lower, upper));
            }
        }

        return value;
    }
}
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.Pair;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import io.whz.synapse.R;
import io.whz.synapse.element.DigitView;
import io.whz.synapse.element.FigureProvider;
import io.whz.synapse.element.Global;
import io.whz.synapse.element.Scheduler;
import io.whz.synapse.neural.MNISTUtil;
import io.whz.synapse.neural.NeuralNetwork;
import io.whz.synapse.pojo.constant.PreferenceCons;
import io.whz.synapse.pojo.constant.TrackCons;
import io.whz.synapse.pojo.dao.DBModel;
import io.whz.synapse.pojo.dao.DBModelDao;
import io.whz.synapse.pojo.neural.Model;
import io.whz.synapse.pojo.multiple.binder.TrainedModelViewBinder;
import io.whz.synapse.pojo.neural.Figure;
import io.whz.synapse.track.ExceptionHelper;
import io.whz.synapse.track.Tracker;
import io.whz.synapse.util.DbHelper;
import io.whz.synapse.util.Precondition;
import static io.whz.synapse.R.id.chart;
import static io.whz.synapse.R.id.predict;
package io.whz.synapse.component;






public class PlayActivity extends WrapperActivity implements View.OnClickListener {
    public static final String ID = "id";

    private static final String TAG = App.TAG + "-PlayActivity";
    private static final int[] FG = TrainedModelViewBinder.FG;
    private static final int[] BG = TrainedModelViewBinder.BG;

    private Toolbar mToolbar;
    private LineChart mChart;
    private DigitView mDigitView;
    private TextView mPredictText;
    private TextView mRateText;
    private View mRefresh;
    private View mPageLoading;

    private BlockTouchListener mBlockListener;
    private NeuralNetwork mNetwork;
    private LineDataSet mLineData;
    private View mLowerBg;
    private FigureProvider mProvider;
    private long mCurId = -1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_play);

        mToolbar = findViewById(R.id.tool_bar);
        mChart = findViewById(chart);
        mDigitView = findViewById(R.id.digit_view);
        mPredictText = findViewById(predict);
        mRateText = findViewById(R.id.predict_rate);
        mRefresh = findViewById(R.id.refresh);
        mPageLoading = findViewById(R.id.page_loading);
        mLowerBg = findViewById(R.id.lower_bg);

        initToolbar();
        init();
    }

    private void init() {
        mRefresh.setOnClickListener(this);

        mBlockListener = new BlockTouchListener(new BlockTouchListener.Callback() {
            @Override
            public void onBlock() {
                startPredicting();

                Tracker.getInstance()
                        .logEvent(TrackCons.Play.PLAY_HAND_WRITE);