카테고리 보관물: Javascript

Javascript

안드로이드 웹뷰와 웹워커 브릿지 (android + webview + webworker bridge example) 샘플

목적

안드로이드에서 웹뷰를 실행한후 브릿지를 이용하여 연동하는 방법과 웹뷰와 웹워커를 이용합니다.

서버에서 받은 TR 을  분석하는 일을 웹워커에게 전달후 TR 분석 작업이 끝나면 json 데이터를 html 페이지에 돌려주는 방법 입니다.

또한 디자인패턴의 테플릿 메소드 패턴을 사용하여 추가되는 TR 분석은 클래스만 만들어서 사용합니다.

하고자 하는 것을 정리해보면 아래와 같습니다.

  1. 웹뷰에서 조회1 클릭
  2. 네이티브로 데이터 전송 ( TrMake.js  window.HybridApp.sendMessage )
  3. 네이티브에서 데이터 받음 ( webView.addJavascriptInterface(new AndroidBridge(), “HybridApp”); )
  4. 네이티브 -> TR 서버 데이터 전송
  5. 서버 TR 응답 데이터 -> 네이티브에서 받음
  6. 네이티브에서 TR 응답데이터 -> 웹뷰로 전송 ( webView.loadUrl(“javascript:native2Webview(‘”+resData+”‘)”); )
  7. 웹뷰에서 데이터 받은 ( native2Webview(trData) )
  8. 받은 TR 데이터 워커로 전송 ( sendWorker(data)  )
  9. 워커에서 TR 데이터 json 변환후 HTML 로 전송 ( sendHtml() { postMessage(this._json); } )
  10. 워커에서 json 로 변환된 데이터 받음 ( receiveWorker() )
  11. 받은데이터 화면에 출력

그림

결과


소스 분석

1. 웹뷰에서 조회1 클릭

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width" />

    <title>web Workers basic example</title>

  </head>

  <body>
    <h1>Web Worker  example</h1>

    <button id="btnStartWorker" class="btn">조회1</button>
    <button id="btnStartWorker2" class="btn">조회2</button>


<div id="output"></div>


    <script>

    const btnStartWorker = document.getElementById( 'btnStartWorker' );
    const btnStartWorker2 = document.getElementById( 'btnStartWorker2' );
    
    const output = document.getElementById( 'output' );                     // 받은 메시지 출력

    let  worker = new Worker( 'worker.js' );

    // native -> 웹뷰 브릿지
    function native2Webview(trData) {
        console.log('index.html >>  native 에서 tr 데이터 수신 : '+trData);
        sendWorker(trData);
    }

    // 워커로 메시지 송신
    function sendWorker(data) {
        worker.postMessage(data);
    }

    // 워커로 메시지 수신
    function receiveWorker() {
        worker.onmessage = function( e ) {
                console.log('index.html >> 워커로부터 json 결과 데이터 수신 ');
                console.log(e.data);
        };
    }
    
    </script>
    <script src="main.js"></script>
    <script src="TrMake.js"></script>
  </body>
</html>

main.js

btnStartWorker.addEventListener( 'click', startWorker );
btnStartWorker2.addEventListener( 'click', startWorker2 );

    function startWorker() {
		const tr = new Tr1111();
		tr.load();
    }

    function startWorker2() {
        const tr = new Tr2222();
        tr.load();
    }

2. 네이티브로 데이터 전송 ( window.HybridApp.sendMessage )

TrMake.js

const TrMake = class {
    constructor() {}
    load() {
        const num = this._load();  // 위임 부분
        // 공통부분
        window.HybridApp.sendMessage("네이티브로 데이터 전송 web > Android 전송 " + num);
        receiveWorker();
    }
    _load(v) { throw "override"; } // HOOK
    receiveWorker() {
        worker.onmessage = function( e ) {
                console.log('index.html >> 워커로부터 json 결과 데이터 수신 ');
                console.log(e.data);
        };
    }
};

const Tr1111 = class extends TrMake {
    constructor() {
        super(' 자식 > 부모 Tr1111');
    }
    _load(v) {
        console.log('tr1111 만들기');
        // TODO
        return 1111;
    }
}

const Tr2222 = class extends TrMake {
    constructor() {
        super(' 자식 > 부모 Tr2222');
    }
    _load(v) {
        console.log('tr2222 만들기');
        // TODO
        return 2222;
    }
}

3. 네이티브에서 데이터 받음 ( webView.addJavascriptInterface(new AndroidBridge(), “HybridApp”); )

..
..
webView.addJavascriptInterface(new AndroidBridge(), "HybridApp");

4. 네이티브 -> TR 서버 데이터 전송
5. 서버 TR 응답 데이터 -> 네이티브에서 받음
6. 네이티브에서 TR 응답데이터 -> 웹뷰로 전송 ( webView.loadUrl(“javascript:native2Webview(‘”+resData+”‘)”); )

webView.loadUrl("javascript:native2Webview('"+resData+"')");

7. 웹뷰에서 데이터 받은 ( native2Webview(trData) )

// native -> 웹뷰 브릿지
function native2Webview(trData) {
    console.log('index.html >>  native 에서 tr 데이터 수신 : '+trData);
    sendWorker(trData);
}

8. 받은 TR 데이터 워커로 전송 ( sendWorker(data) )

// 워커로 메시지 송신
function sendWorker(data) {
    worker.postMessage(data);
}

9.워커에서 TR 데이터 json 변환후 HTML 로 전송 ( sendHtml() { postMessage(this._json); } )

const Worker = class {
    constructor() {}
    load(v) {
        this._json = this._load(v);  // 위임 부분
        // 공통부분
        this.sendHtml();
    }
    _load(v) { throw "override"; } // HOOK
    sendHtml() { postMessage(this._json); }
    log(v) { console.log('worker >> '+v); }
};

10. 워커에서 json 로 변환된 데이터 받음 ( receiveWorker() )

// 워커로 메시지 수신
function receiveWorker() {
    worker.onmessage = function( e ) {
            console.log('index.html >> 워커로부터 json 결과 데이터 수신 ');
            console.log(e.data);
    };
}

11. 받은데이터 화면에 출력

전체소스

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width" />

    <title>web Workers basic example</title>

  </head>

  <body>
    <h1>Web Worker  example</h1>

    <button id="btnStartWorker" class="btn">조회1</button>
    <button id="btnStartWorker2" class="btn">조회2</button>


<div id="output"></div>


    <script>

    const btnStartWorker = document.getElementById( 'btnStartWorker' );
    const btnStartWorker2 = document.getElementById( 'btnStartWorker2' );

    const output = document.getElementById( 'output' );                     // 받은 메시지 출력

    let  worker = new Worker( 'worker.js' );


    // native -> 웹뷰 브릿지
    function native2Webview(trData) {
        console.log('index.html >>  native 에서 tr 데이터 수신 : '+trData);
        sendWorker(trData);
    }

    // 워커로 메시지 송신
    function sendWorker(data) {
        worker.postMessage(data);
    }

    // 워커로 메시지 수신
    function receiveWorker() {
        worker.onmessage = function( e ) {
                console.log('index.html >> 워커로부터 json 결과 데이터 수신 ');
                console.log(e.data);
        };
    }
    
    
/*
 . 조회1 클릭
 . 네이티브로 데이터 전송 ( TrMake.js  window.HybridApp.sendMessage )
 . 네이티브에서 데이터 받음 ( webView.addJavascriptInterface(new AndroidBridge(), "HybridApp"); )
 . 네이티브 -> TR 서버 데이터 전송
 . 서버 TR 응답 데이터 -> 네이티브에서 받음
 . 네이티브에서 TR 응답데이터 -> 웹뷰로 전송 ( webView.loadUrl("javascript:native2Webview('"+resData+"')"); )
 . 웹뷰에서 데이터 받은 ( native2Webview(trData) )
 . 받은 TR 데이터 워커로 전송 ( sendWorker(data)  )
 . 워커에서 TR 데이터 json 변환후 HTML 로 전송 ( sendHtml() { postMessage(this._json); } )
 . 워커에서 json 로 변환된 데이터 받음 ( receiveWorker() )
 . 받은데이터 화면에 출력
*/



    </script>
    <script src="main.js"></script>
    <script src="TrMake.js"></script>
  </body>
</html>

main.js

btnStartWorker.addEventListener( 'click', startWorker );
btnStartWorker2.addEventListener( 'click', startWorker2 );


    function startWorker() {
	const tr = new Tr1111();
	tr.load();
    }

    function startWorker2() {
        const tr = new Tr2222();
        tr.load();
    }

TrMake.js

const TrMake = class {
    constructor() {}
    load() {
        const num = this._load();  // 위임 부분
        // 공통부분
        window.HybridApp.sendMessage("네이티브로 데이터 전송 web > Android 전송 " + num);
        receiveWorker();
    }
    _load(v) { throw "override"; } // HOOK
    receiveWorker() {
        worker.onmessage = function( e ) {
                console.log('index.html >> 워커로부터 json 결과 데이터 수신 ');
                console.log(e.data);
        };
    }
};

const Tr1111 = class extends TrMake {
    constructor() {
        super(' 자식 > 부모 Tr1111');
    }
    _load(v) {
        console.log('tr1111 만들기');
        // TODO
        return 1111;
    }
}

const Tr2222 = class extends TrMake {
    constructor() {
        super(' 자식 > 부모 Tr2222');
    }
    _load(v) {
        console.log('tr2222 만들기');
        // TODO
        return 2222;
    }
}

worker.js

const Worker = class {
    constructor() {}
    load(v) {
        this._json = this._load(v);  // 위임 부분
        // 공통부분
        this.sendHtml();
    }
    _load(v) { throw "override"; } // HOOK
    sendHtml() { postMessage(this._json); }
    log(v) { console.log('worker >> '+v); }
};

const WTr1111 = class extends Worker {
    constructor() {
        super(' 자식 > 부모 Tr1111');
    }
    _load(v) {
        this.log('WTr1111 분석하기:'+v);
        // TODO
        const json = "{'tr':'1111','flag':'ok','data':'"+v+"'}";
        return json;
    }
}

const WTr2222 = class extends Worker {
    constructor() {
        super(' 자식 > 부모 Tr2222');
    }
    _load(v) {
        this.log('WTr2222 분석하기:'+v);
        // TODO
        const json = "{'tr':'2222','flag':'ok','data':'"+v+"'}";
        return json;
    }
}

const parseJson = data => {
    if(data.indexOf('11 11 11 11') >=0) {
        const wtr = new WTr1111();
        wtr.load(data);
    }else if(data.indexOf('22 22 22 22') >=0) {
        const wtr = new WTr2222();
        wtr.load(data);
    }
};

// HTML 에서 받은 데이터
self.onmessage = e => {
    parseJson(e.data);
};

mainActivity.java

package com.example.worker_bridge;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.StrictMode;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class MainActivity extends AppCompatActivity {

    private WebView webView;
    TextView mTextView;

    public TextView Toptext;
    public TextView datatext;
    public TextView byText;
    public Button StartButton;
    public Button StopButton;
    public Button ConnButton;
    public Button DiconButton;
    public Button IsconButton;
    public Button Lilly;
    private Socket socket;
    // fixme: TAG
    String TAG = "socketTest";

    private final Handler handler = new Handler();

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

        mTextView = findViewById(R.id.mTextView);

        ConnButton = findViewById(R.id.button1);
        StartButton = findViewById(R.id.button2);
        StopButton = findViewById(R.id.button3);
        DiconButton = findViewById(R.id.button4);
        IsconButton = findViewById(R.id.button5);
        Lilly = findViewById(R.id.button6);
        final EditText ipNumber = findViewById(R.id.ipText);



        CookieManager cookieManager = CookieManager.getInstance();


        webView = (WebView)findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);


        webView.clearCache(true);
        webView.loadUrl("http://devtest.xxx.com/android_worker/android_worker.html");

        webView.addJavascriptInterface(new AndroidBridge(), "HybridApp");
        webView.setWebChromeClient(new WebChromeClient());      // Javascript alert 기능


        Log.i(TAG, "Application createad");

        int SDK_INT = android.os.Build.VERSION.SDK_INT;
        if (SDK_INT > 8) {
            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
            StrictMode.setThreadPolicy(policy);
        }


        ConnButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(), "Connect 시도", Toast.LENGTH_SHORT).show();
                String addr = ipNumber.getText().toString().trim();
                ConnectThread thread = new ConnectThread(addr);

                //키보드 자동 내리기
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(ipNumber.getWindowToken(), 0);

                thread.start();


            }
        });

        // fixme: 버튼 ClickListener
        StartButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                StartThread sthread = new StartThread();
                StartButton.setEnabled(false);
                StopButton.setEnabled(true);

                sthread.start();

            }
        });
        StopButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                StopThread spthread = new StopThread();
                StartButton.setEnabled(true);
                StopButton.setEnabled(false);
                spthread.start();
            }
        });
        DiconButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    socket.close();
                    Toast.makeText(getApplicationContext(), "DisConnect", Toast.LENGTH_SHORT).show();
                    DiconButton.setEnabled(false);
                    ConnButton.setEnabled(true);
                    StartButton.setEnabled(false);
                    StopButton.setEnabled(false);
                } catch (IOException e) {
                    e.printStackTrace();
                    Toast.makeText(getApplicationContext(), "DisConnect 실패", Toast.LENGTH_SHORT).show();
                }
            }
        });
        IsconButton.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                boolean iscon = socket.isClosed();
                InetAddress addr = socket.getInetAddress();
                String tmp = addr.getHostAddress();
                if(!iscon){
                    Toast.makeText(getApplicationContext(), tmp + " 연결 중", Toast.LENGTH_SHORT).show();
                }
                else{
                    Toast.makeText(getApplicationContext(), "연결이 안 되어 있습니다.", Toast.LENGTH_SHORT).show();
                }
            }
        });
        Lilly.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getApplicationContext(), " Lilly is Cute.\n Lilly is working hard.", Toast.LENGTH_SHORT).show();
            }
        });

    }


    private class AndroidBridge {
        @JavascriptInterface
        public void sendMessage(final String arg) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    System.out.println("native >> " + arg);
                    mTextView.setText(arg);

                    String resData = "";
                    if(arg.contains("1111")) {
                        resData = "11 11 11 11 54 41 52 54 0a";
                    } else if(arg.contains("2222")) {
                        resData = "22 22 22 22 41 52 54 0a";
                    }

                    /*
                       네이티브 -> TR 서버 데이터 전송
                       서버 TR 응답 데이터 -> 네이티브에서 받음
                    */
                    //네이티브에서 TR 응답데이터 -> 웹뷰로 전송
                    webView.loadUrl("javascript:native2Webview('"+resData+"')");


                }
            });
        }
    }


    // fixme: Start 버튼 클릭 시 데이터 송/수신.
    class StartThread extends Thread{

        int bytes;
        String Dtmp;
        int dlen;

        public StartThread(){

            datatext = findViewById(R.id.recvByte);
            byText = findViewById(R.id.ByteText);
        }


        public String byteArrayToHex(byte[] a) {
            StringBuilder sb = new StringBuilder();
            for(final byte b: a)
                sb.append(String.format("%02x ", b&0xff));
            return sb.toString();
        }

        public void run(){

            // 데이터 송신
            try {

                String OutData = "AT+START\n";
                byte[] data = OutData.getBytes();
                OutputStream output = socket.getOutputStream();
                output.write(data);
                Log.d(TAG, "AT+START\\n COMMAND 송신");

            } catch (IOException e) {
                e.printStackTrace();
                Log.d(TAG,"데이터 송신 오류");
            }

            // 데이터 수신
            try {
                Log.d(TAG, "데이터 수신 준비");

                //TODO:수신 데이터(프로토콜) 처리

                while (true) {
                    byte[] buffer = new byte[1024];

                    InputStream input = socket.getInputStream();

                    bytes = input.read(buffer);
                    Log.d(TAG, "byte = " + bytes);

                    //바이트 헥사(String)로 바꿔서 Dtmp String에 저장.
                    Dtmp = byteArrayToHex(buffer);
                    Dtmp = Dtmp.substring(0,bytes*3);
                    Log.d(TAG, Dtmp);


                    String obj = (String) Dtmp;
                    webDataSend(obj);


                    //프로토콜 나누기
                    String[] DSplit = Dtmp.split("a5 5a"); // sync(2byte) 0xA5, 0x5A
                    Dtmp = "";
                    for(int i=1;i<DSplit.length-1;i++){ // 제일 처음과 끝은 잘림. 데이터 버린다.
                        Dtmp = Dtmp + DSplit[i] + "\n";
                    }
                    dlen =  DSplit.length- 2;

                    /*
                    runOnUiThread(new Runnable() {
                        public void run() {
                            datatext.setText(Dtmp);
                            byText.setText("데이터 " + dlen + "개");
                        }
                    });
                    */



                }
            }catch(IOException e){
                e.printStackTrace();
                Log.e(TAG,"수신 에러");
            }


        }

    }

    public void webDataSend(String obj) {
        System.out.println("웹으로 데이터 전송........................................!!!!!!!!!!!!!!!!!!");
        webView.post(new Runnable() {
            @Override
            public void run() {
                webView.loadUrl("javascript:AndroidToSend('안드로이드에서 받은 메시지 = "+obj+"')");
            }
        });
    }

    // fixme: Stop 버튼 클릭 시 데이터 송신.
    class StopThread extends Thread{


        public StopThread(){
        }

        public void run(){

            // 데이터 송신
            try {

                String OutData = "AT+STOP\n";
                byte[] data = OutData.getBytes();
                OutputStream output = socket.getOutputStream();
                output.write(data);
                Log.d(TAG, "AT+STOP\\n COMMAND 송신");

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



        }

    }
    // fixme: Socket Connect.
    class ConnectThread extends Thread {
        String hostname;

        public ConnectThread(String addr) {
            hostname = addr;
        }

        public void run() {
            try { //클라이언트 소켓 생성

                int port = 25003;
                socket = new Socket(hostname, port);
                Log.d(TAG, "Socket 생성, 연결.");

                Toptext = findViewById(R.id.text1);

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        InetAddress addr = socket.getInetAddress();
                        String tmp = addr.getHostAddress();
                        Toptext.setText(tmp + " 연결 완료");
                        Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_LONG).show();

                        DiconButton.setEnabled(true);
                        ConnButton.setEnabled(false);
                        StartButton.setEnabled(true);
                    }
                });




            } catch (UnknownHostException uhe) { // 소켓 생성 시 전달되는 호스트(www.unknown-host.com)의 IP를 식별할 수 없음.

                Log.e(TAG, " 생성 Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 호스트의 IP 주소를 식별할 수 없음.(잘못된 주소 값 또는 호스트 이름 사용)");
                    }
                });

            } catch (IOException ioe) { // 소켓 생성 과정에서 I/O 에러 발생.

                Log.e(TAG, " 생성 Error : 네트워크 응답 없음");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 네트워크 응답 없음", Toast.LENGTH_SHORT).show();
                        Toptext.setText("네트워크 연결 오류");
                    }
                });


            } catch (SecurityException se) { // security manager에서 허용되지 않은 기능 수행.

                Log.e(TAG, " 생성 Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 보안(Security) 위반에 대해 보안 관리자(Security Manager)에 의해 발생. (프록시(proxy) 접속 거부, 허용되지 않은 함수 호출)");
                    }
                });


            } catch (IllegalArgumentException le) { // 소켓 생성 시 전달되는 포트 번호(65536)이 허용 범위(0~65535)를 벗어남.

                Log.e(TAG, " 생성 Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)");
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(getApplicationContext(), " Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)", Toast.LENGTH_SHORT).show();
                        Toptext.setText("Error : 메서드에 잘못된 파라미터가 전달되는 경우 발생.(0~65535 범위 밖의 포트 번호 사용, null 프록시(proxy) 전달)");
                    }
                });


            }




        }
    }




    @Override
    protected void onStop() {  //앱 종료시
        super.onStop();
        try {
            socket.close(); //소켓을 닫는다.
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

AndroidMainfest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:usesCleartextTraffic="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Worker_bridge"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <WebView
        android:id="@+id/webview"
        android:layout_width="407dp"
        android:layout_height="381dp"
        android:layout_marginTop="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ByteText"
        tools:ignore="MissingConstraints"
        tools:layout_editor_absoluteX="1dp" />

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="60dp"
        android:text="IP 주소 입력 후 Connect 버튼을 눌러 연결을 시도하세요."
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />

    <EditText
        android:id="@+id/ipText"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="80dp"
        android:text="220.72.212.247"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="120dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints">

        <Button
            android:id="@+id/button1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="true"
            android:text="Connect" />

        <Button
            android:id="@+id/button2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="START" />

        <Button
            android:id="@+id/button3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="STOP" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="160dp"
        android:orientation="horizontal"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="MissingConstraints">

        <Button
            android:id="@+id/button4"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:enabled="false"
            android:text="Disconnect" />

        <Button
            android:id="@+id/button5"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:text="Connect확인" />

        <Button
            android:id="@+id/button6"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:text="LIILY" />
    </LinearLayout>
    <TextView
        android:paddingTop="20dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:id="@+id/ByteText"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints" />
    <TextView
        android:paddingTop="30dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:id="@+id/recvByte"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/mTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="TextView"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout"
        tools:layout_editor_absoluteX="176dp" />
</androidx.constraintlayout.widget.ConstraintLayout>