Paint Functionality

PaintActivity.java

OnCreate Activity

public class PaintActivity extends AppCompatActivity {

    // Permission for read and write
    private static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 0;
    private static final int MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 0;

    // views
    private List<ImageView> colorButtons;
    private ConstraintLayout sizeButton;
    private boolean isSizeOpen;
    private boolean isColorOpen;

    // after pairing, jump to chat with firebase
    private FirebaseAuth mAuth;
    private FirebaseUser user;
    private CustomWebSocket client;
    private CustomWebSocket mClient;
    // uuid for image transfer to ccyy.xyz
    private String uuid;

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

        // request read and write permission
        requestStoragePermission();

        // get instance for firebase
        mAuth = FirebaseAuth.getInstance();
        user = mAuth.getCurrentUser();
        client = CustomWebSocket.getInstance();


        // view related
        sizeButton = findViewById(R.id.size_buttons);

        colorButtons = new ArrayList<>();
        colorButtons.add(findViewById(R.id.blue_button));
        colorButtons.add(findViewById(R.id.green_button));
        colorButtons.add(findViewById(R.id.orange_button));
        colorButtons.add(findViewById(R.id.pink_button));

        isSizeOpen = true;
        isColorOpen = true;
    }
}

Functionality 1: Change paint color

    /**
     * change paint color
     *
     * @param view to click
     */
    public void selectColors(View view) {
        DrawView drawView = findViewById(R.id.draw_view);
        if (view.getId() == R.id.blue_button) {
            drawView.changeColor(getResources().getColor(R.color.blue));
        } else if (view.getId() == R.id.green_button) {
            drawView.changeColor(getResources().getColor(R.color.green));
        } else if (view.getId() == R.id.orange_button) {
            drawView.changeColor(getResources().getColor(R.color.orange));
        } else if (view.getId() == R.id.pink_button) {
            drawView.changeColor(getResources().getColor(R.color.pink));
        }
        showColors(view);
    }

    /**
     * show or hide color panel
     *
     * @param view to show or hide
     */
    public void showColors(View view) {
        if (isColorOpen) {
            for (int i = 0; i < colorButtons.size(); i++) {
                ImageView colorButton = colorButtons.get(i);
                colorButton.animate().translationYBy(-170f * (i + 1))
                        .setDuration(300)
                        .setInterpolator(new DecelerateInterpolator())
                        .start();
            }
        } else {
            for (int i = 0; i < colorButtons.size(); i++) {
                ImageView colorButton = colorButtons.get(i);
                colorButton.animate().translationYBy(170f * (i + 1))
                        .setDuration(300)
                        .setInterpolator(new DecelerateInterpolator())
                        .start();
            }
        }
        isColorOpen = !isColorOpen;
    }

Functionality 2: Change paint size

    /**
     * change paint size
     *
     * @param view size view
     */
    public void selectSizes(View view) {
        DrawView drawView = findViewById(R.id.draw_view);
        if (view.getId() == R.id.s_button) {
            drawView.changeSize(10f);
        } else if (view.getId() == R.id.m_button) {
            drawView.changeSize(15f);
        } else if (view.getId() == R.id.l_button) {
            drawView.changeSize(20f);
        } else if (view.getId() == R.id.x_button) {
            drawView.changeSize(25f);
        }
        if (sizeButton.getVisibility() == View.VISIBLE) {
            showSizes(view);
        }
    }

    /**
     * show or hide size panel
     *
     * @param view to show or hide
     */
    public void showSizes(View view) {
        if (isSizeOpen) {
            sizeButton.setVisibility(View.VISIBLE);
            TranslateAnimation anim = new TranslateAnimation(
                    Animation.ABSOLUTE, 0f,
                    Animation.ABSOLUTE, 0f,
                    Animation.RELATIVE_TO_SELF, 1f,
                    Animation.RELATIVE_TO_SELF, 0f
            );
            anim.setDuration(300);
            anim.setFillAfter(true);
            anim.setInterpolator(new DecelerateInterpolator());

            sizeButton.startAnimation(anim);
        } else {
            sizeButton.setVisibility(View.GONE);
            TranslateAnimation anim = new TranslateAnimation(
                    Animation.ABSOLUTE, 0f,
                    Animation.ABSOLUTE, 0f,
                    Animation.RELATIVE_TO_SELF, 0f,
                    Animation.RELATIVE_TO_SELF, 1f
            );
            anim.setDuration(300);
            anim.setFillAfter(true);
            anim.setInterpolator(new DecelerateInterpolator());

            sizeButton.startAnimation(anim);

        }
        isSizeOpen = !isSizeOpen;
    }

Functionality 3: Erase lines

    /**
     * erase lines
     *
     * @param view the erase view
     */
    public void erase(View view) {
        DrawView drawView = findViewById(R.id.draw_view);
        drawView.erase();
    }

Functionality 4: Undo lines

    /**
     * undo a line
     *
     * @param view the undo view
     */
    public void undo(View view) {
        DrawView drawView = findViewById(R.id.draw_view);
        drawView.undo();
    }

Functionality 5: Save Image

    /**
     * save image to media with new uuid
     *
     * @param view
     */
    public void saveImage(View view) {
        DrawView drawView = findViewById(R.id.draw_view);
        drawView.setDrawingCacheEnabled(true);
        uuid = UUID.randomUUID().toString();
        String imageSaved = MediaStore.Images.Media.insertImage(
                getContentResolver(),
                drawView.createBitmap(),
                uuid + ".png",
                "myPainting");

        if (imageSaved != null) {
            Toast.makeText(getApplicationContext(), "ImageSaved", Toast.LENGTH_SHORT).show();
            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                // send image to ccyy.xyz
                public void run() {
                    sendImage(uuid, imageSaved);
                }
            }, 1000);

        } else {
            Toast.makeText(getApplicationContext(), "NoImageSaved", Toast.LENGTH_SHORT).show();
        }
    }

Functionality 6: Upload image to backend HTTP image server

    /**
     * send image to ccyy.xyz
     *
     * @param uuid       the uuid of the image
     * @param imageSaved the real image
     */
    public void sendImage(String uuid, String imageSaved) {

        try {
            // use https multipart to send png files
            new MultipartUploadRequest(this, uuid, "https://ccyy.xyz/api/v2/post/" + uuid)
                    // get file from temp
                    .addFileToUpload(Environment.getExternalStorageDirectory() + "/temp.png", "file")
                    // POST to server
                    .setMethod("POST")
                    // listen on progress
                    .setDelegate(new UploadStatusDelegate() {
                        @Override
                        public void onProgress(Context context, UploadInfo uploadInfo) {
                            Toast.makeText(context, "Transmission in progress", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onError(Context context, UploadInfo uploadInfo, Exception exception) {
                            Toast.makeText(context, "Transmission failed", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) {
                            Toast.makeText(context, "Transmission success", Toast.LENGTH_SHORT).show();
                            Handler handler = new Handler();
                            handler.postDelayed(new Runnable() {
                                // after image successfully send to ccyy.xyz using https
                                // tell server to predict my paint and pair
                                public void run() {
                                    tellServer();
                                }
                            }, 1000);
                        }

                        @Override
                        public void onCancelled(Context context, UploadInfo uploadInfo) {
                            Toast.makeText(context, "Transmission cancel", Toast.LENGTH_SHORT).show();
                        }
                    })
                    // start upload()
                    .startUpload();

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

Functionality 7: Send new friend pairing request to WebSocket Server

    /**
     * Websocket listener
     */
    private final class EchoWebSocketListener extends WebSocketListener {
        private static final int CLOSE_STATUS = 1000;

        @Override
        public void onOpen(WebSocket webSocket, Response response) {
            // send email and uid and image uuid in json string to ccyy.xyz, to further predict and pair
            webSocket.send(String.format("{\"Proto\":1,\"Proto1\":5,\"PlayerName\":\"%s\",\"PlayerId\":\"%s\",\"Img\":\"%s\"}",
                    user.getEmail(),
                    user.getUid(),
                    uuid + ".png"));
        }

        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        public void onMessage(WebSocket webSocket, String message) {
            // if reply coming back from websocket, decode it and parse it
            // extract user and userid for chat
            Base64.Decoder decoder = Base64.getMimeDecoder();
            try {
                String jsonString = new String(decoder.decode(message), "UTF-8");
                JSONObject json = new JSONObject(jsonString);
                String playerName = json.getString("PlayerName");
                String playerId = json.getString("PlayerId");
                Intent intent = new Intent();
                intent.setClass(getApplicationContext(), ChatBoxActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putExtra("user", playerName);
                intent.putExtra("userId", playerId);
                startActivity(intent);

            } catch (UnsupportedEncodingException | JSONException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onMessage(WebSocket webSocket, ByteString bytes) {
            print("Receive Bytes : " + bytes.hex());
        }

        @Override
        public void onClosing(WebSocket webSocket, int code, String reason) {
            webSocket.close(CLOSE_STATUS, null);
            print("Closing Socket : " + code + " / " + reason);
        }

        @Override
        public void onFailure(WebSocket webSocket, Throwable throwable, Response response) {
            print("Error : " + throwable.toString());
        }
    }

    /**
     * after sending image to server, tell server to predict and pair
     */
    private void tellServer() {
        // build the request
        // inject a fake header to pass ccyy.xyz server's nginx checking, otherwise receive 403 forbidden :(
        // use wss (ws with ssl) for security!
        Request request = new Request.Builder().url("wss://ccyy.xyz/api/v3/")
                .removeHeader("User-Agent")
                .addHeader("Cache-Control", "no-cache")
                .addHeader("Connection", "Upgrade")
                .addHeader("Host", "ccyy.xyz")
                .addHeader("Origin", "https://ccyy.xyz")
                .addHeader("Pragma", "no-cache")
                .addHeader("Upgrade", "websocket")
                .addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
                .build();

        // set up my websocket request listener
        EchoWebSocketListener listener = new EchoWebSocketListener();
        // init websocket
        WebSocket webSocket = client.getClient().newWebSocket(request, listener);
        // execute my request
        client.getClient().dispatcher().executorService();//.shutdown();
    }
}

Helper method 1: request read / write permission for android version >= M

    /**
     * request permission for read and store
     */
    private void requestStoragePermission() {
        // only require this after android M
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                        MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
            }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
            }
        }

    }

Helper method 2: print debug info on image.

    /**
     * debug print for child threads, need to print on ui threads
     *
     * @param message the message to print
     */
    private void print(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                TextView textResult = findViewById(R.id.text_result);
                textResult.setText(textResult.getText().toString() + "\n" + message);
            }
        });
    }

PathLine.java

public class PathLine {

    Path path;
    int color;
    float size;


    public PathLine(Path path, int color, float size) {
        this.path = path;
        this.color = color;
        this.size = size;
    }
}

DrawView.java

public class DrawView extends View {
    // default color and size
    private int currentColor = getResources().getColor(R.color.blue);
    private float currentSize = 10f;

    private Paint myPaint;
    private Path myPath;
    private final List<PathLine> lines;

    public DrawView(Context context) {
        super(context);
        lines = new ArrayList<>();
        initPaint();
    }

    public DrawView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        lines = new ArrayList<>();
        initPaint();
    }

    /**
     * initialise painting config
     */
    public void initPaint() {
        myPaint = new Paint();
        myPaint.setColor(currentColor);
        myPaint.setStrokeWidth(currentSize);
        myPaint.setStrokeJoin(Paint.Join.ROUND);
        myPaint.setStrokeCap(Paint.Cap.ROUND);
        myPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // draw previous lines first
        for (PathLine pathLine : lines) {
            myPaint.setColor(pathLine.color);
            myPaint.setStrokeWidth(pathLine.size);
            canvas.drawPath(pathLine.path, myPaint);
        }
        // draw new line
        if (myPath != null) {
            myPaint.setColor(currentColor);
            myPaint.setStrokeWidth(currentSize);
            canvas.drawPath(myPath, myPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // init path at x, y
                myPath = new Path();
                myPath.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                // move to x, y
                myPath.lineTo(event.getX(), event.getY());
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                // add my line to lines
                lines.add(new PathLine(myPath, currentColor, currentSize));
                myPath = null;
                break;
        }
        return true;
    }
}

change size

    /**
     * change size to size
     *
     * @param size the size
     */
    public void changeSize(float size) {
        currentSize = size;
    }

change color

    /**
     * change color to color
     *
     * @param color the color
     */
    public void changeColor(int color) {
        currentColor = color;
    }

erase

    /**
     * use white line to erase
     */
    public void erase() {
        currentColor = Color.WHITE;
    }

undo

    /**
     * remove the last line
     */
    public void undo() {
        if (lines.size() > 0) {
            lines.remove(lines.size() - 1);
        }
        invalidate();
    }

createBitmap

    /**
     * save my lines to a bitmap and save to disk
     *
     * @return the bitmap
     */
    public Bitmap createBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(this.getWidth(), this.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        for (PathLine pathLine : lines) {
            myPaint.setColor(pathLine.color);
            myPaint.setStrokeWidth(pathLine.size);
            canvas.drawPath(pathLine.path, myPaint);
        }

        // save to temp
        File file = new File(Environment.getExternalStorageDirectory() + "/temp.png");
        try {
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bitmap;
    }