목적 : 안드로이드 webview 와 native 간의 bridge 를 이용하여 연결하는 방법을 알아보도록 하겠습니다.
또한 native 는 TCP Socket 을 이용하여 데이터 전송을 할 예정입니다.
다소 복잡할것 같지만 소스 보면서 확인하시죠
안드로이드
AndroidManifest.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>
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.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); webView = (WebView)findViewById(R.id.webview); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("http://devtest.coforward.com/andrioWebSocket.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() { mTextView.setText(arg); } }); } } // 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(); } } }
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>
Socket 서버 (php)
<?php $host = "0.0.0.0"; $port = 25003; set_time_limit(0); // no timeout $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket\n"); $result = socket_bind($socket, $host, $port) or die("Could not bind to socket\n"); $result = socket_listen($socket, 3) or die("Could not set up socket listener\n"); do { $spawn = socket_accept($socket) or die("Could not accept incoming connection\n"); do { $input = socket_read($spawn, 1024) or die("Could not read input\n"); $input = trim($input); echo "Client Message : " . $input . "\n"; //$response = 'received' . "\n"; $response = $input ."\n"; socket_write($spawn, $response, strlen($response)) or die("Could not write output\n"); if ($input == 'quit') { break; } if ($input == 'shutdown') { socket_close($spawn); break 2; } } while (true); } while (true); socket_close($socket);
소켓서버 시작
socket_server]# php server2_php.php
https://ddangeun.tistory.com/31