天天看點

無網絡PC通過USB與多個Android裝置通訊

通過ADB将USB模拟為網卡,建立Socket進行通訊

    • 前言
        • 應用場景
        • 實作效果
        • 實作思路
    • Android服務端實作
        • MainActivity.java
        • TcpConnectRunnable.java
    • PC用戶端實作
        • FrmClient.cs
        • SocketClient.cs
        • DriverDetector.cs
        • ADB操作
    • 運作效果
        • Android服務端
        • PC用戶端
    • 參考資料

前言

應用場景

适用于工作環境無網絡,隻能通過USB将多台Android端資料上傳到一台PC端的情況。

實作效果

  1. 啟動PC端工作站
  2. 自動檢測通過USB連接配接的Android端裝置
  3. 自動啟動Android端資料上傳app
  4. 通過Socket向Android端發送指令
  5. Android端通過Socket上傳資料
  6. 上傳完成後自動關閉app

PC端工作站可長時間開啟,外業人員工作回來後,将裝置插到電腦上即可,上傳操作将會自動執行,可同時插入多台Android裝置。

實作思路

  • 通過ADB将USB模拟為網卡
  • Android端作為服務端,建立ServerSocket,監聽用戶端指令
  • PC端作為用戶端,請求建立Socket連接配接,向Android端發送指令

Android服務端實作

MainActivity.java

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView tvText;

    private TcpConnectRunnable tcpConnectRunnable = new TcpConnectRunnable(new TcpConnectRunnable.Callback() {
        @Override
        public void call(final String msg) {
            // 回調資訊顯示到消息輸出視窗
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tvText.append(msg + "\r\n");
                    if (msg.contains("quit")) {
                        finish();
                    }
                }
            });
        }
    });

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

        tvText = findViewById(R.id.tv_message);

        // 清空消息輸出視窗
        findViewById(R.id.btn_clear).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tvText.setText("");
            }
        });

        // 開啟Socket伺服器
        new Thread(tcpConnectRunnable).start();
    }

    @Override
    protected void onDestroy() {
        tcpConnectRunnable.stop();
        super.onDestroy();
    }
}
           

TcpConnectRunnable.java

import android.text.TextUtils;
import android.util.Log;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Socket伺服器,持續監聽用戶端連接配接
 */
public class TcpConnectRunnable implements Runnable {

    private static final String TAG = TcpConnectRunnable.class.getSimpleName();
    private final int SERVER_PORT = 10086;
    private ServerSocket serverSocket;
    private Socket client;
    private Callback callback;

    private boolean isRun = false;

    public TcpConnectRunnable(Callback callback) {
        this.callback = callback;
    }

    /**
     * 停止
     */
    public void stop() {
        isRun = false;
    }

    @Override
    public void run() {
        isRun = true;

        try {
            String ip = InetAddress.getLocalHost().getHostAddress();
            serverSocket = new ServerSocket(SERVER_PORT);
            callback.call("建立伺服器:[" + ip + ":" + SERVER_PORT + "]");
        }catch (IOException e) {
            callback.call("建立伺服器異常:" + e.getMessage());
        }

        while (isRun) {
            BufferedOutputStream out = null;
            BufferedReader in = null;
            try {
                client = serverSocket.accept();
                callback.call("建立連接配接:" + client.getInetAddress().toString() + ":" + client.getPort());
                out = new BufferedOutputStream(client.getOutputStream());
                in = new BufferedReader(new InputStreamReader(client.getInputStream()));

                if (isRun == false) {
                    break;
                }
                String request = receive(in);
                if (TextUtils.isEmpty(request))
                {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                callback.call("client: " + request);

                if ("quit".equals(request)) {
                    callback.call("origin request: " + request);
                    break;
                }

                if (isRun == false) {
                    break;
                }
                send(out, request);
            } catch (IOException e) {
                Log.e(TAG, "run: ", e);
                callback.call(e.getMessage());
            } finally {
                close(out);
                close(in);
                close(client);
            }
        }
    }

    /**
     * 向用戶端發送資料
     * @param out
     * @param msg
     * @throws IOException
     */
    private void send(OutputStream out, String msg) throws IOException {
        msg += "\n";
        out.write(msg.getBytes("utf-8"));
    }

    /**
     * 接收用戶端的請求,并傳回應答
     * @param in
     * @return
     * @throws IOException
     */
    private String receive(BufferedReader in) throws IOException {
        String r = in.readLine();
        if (TextUtils.isEmpty(r)) {
            return "";
        }
        callback.call("origin request: " + r);
        if (r.contains("upload")) {
            r = r + " finished.";
        }
        return r;
    }

    private void close(OutputStream out) {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void close(BufferedReader in) {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void close(Socket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                Log.e(TAG, "run: ", e);
            }
        }
    }

    /**
     * 消息回調,用于更新UI
     */
    public interface Callback {
        /**
         * 回調
         * @param msg
         */
        void call(String msg);
    }
}
           

PC用戶端實作

FrmClient.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Forms;

namespace PhoneAidClient
{
    public partial class FrmClient : Form
    {
        /// <summary>
        /// 裝置插拔檢測的最小間隔(毫秒)
        /// 用于避免插入裝置觸發多次事件的情況
        /// </summary>
        private static long DEVICE_DETECT_MIN_INTERVAL = 1000;

        /// <summary>
        /// 用于管理ADB指令等
        /// </summary>
        private CmdHelp cmdHelp = new CmdHelp();

        /// <summary>
        /// adb接收到資料的事件,一般隻用在adb的開啟關閉上,其他時候資料都由其他地方處理
        /// </summary>
        private CmdHelp.DelegateReceiveThreadData m_receiveDelegate = null;

        /// <summary>
        /// 系統資訊的處理類
        /// </summary>
        private DriveDetector detector = new DriveDetector();

        /// <summary>
        /// 插入的裝置清單
        /// </summary>
        private List<string> devices = new List<string>();

        /// <summary>
        /// 上次檢測到裝置插拔的時間(毫秒)
        /// </summary>
        private long lastDeviceDetectTime = -1;

        /// <summary>
        /// 用于鎖住ADB的變量,否則一邊作業一遍連接配接usb等操作,可能導緻ADB崩潰
        /// </summary>
        private object m_lock = string.Empty;

        /// <summary>
        /// 本地端口清單
        /// </summary>
        private List<int> localPorts = new List<int>();

        /// <summary>
        /// 随機端口
        /// </summary>
        private Random randomPort = new Random();

        public FrmClient()
        {
            InitializeComponent();
        }

        private void FrmClient_Load(object sender, EventArgs e)
        {
            //防止adb未完全關閉
            Process[] adbList = Process.GetProcessesByName("adb");
            foreach (Process adb in adbList)
            {
                adb.Kill();
                adb.Close();
                adb.Dispose();
            }

            treeDevice.Nodes.Clear();
            treeDevice.Nodes.Add("已連接配接裝置");
            treeDevice.ExpandAll();
            
            //注冊事件
            CmdHelp.EventReceiveData += new CmdHelp.DelegateReceiveData(CmdHelp_EventReceiveData);
            m_receiveDelegate = new CmdHelp.DelegateReceiveThreadData(CmdHelp_EventReceiveThreadData);
            CmdHelp.EventReceiveThreadData += m_receiveDelegate;
            CmdHelp.EventReceiveThreadErrorData += new CmdHelp.DelegateReceiveThreadErrorData(CmdHelp_EventReceiveThreadErrorData);
            
            //啟動ADB服務
            Thread openADBThread = new Thread(new ThreadStart(() =>
            {
                //先關閉,再開啟,防止上一次的意外退出
                cmdHelp.SendAdbCmd(CmdAdbInfo.adb_start_server);
            }));
            openADBThread.Start();

            //搜尋軟體啟動前插入的裝置
            StartSearchDevice();

            //對系統的裝置拔入拔出進行監聽
            detector.DeviceArrived += new EventHandler<DriveDetector.DriveDectctorEventArgs>((obj, ee) =>
            {
                long now = (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000;
                if (lastDeviceDetectTime > 0)
                {
                    if (now - lastDeviceDetectTime < DEVICE_DETECT_MIN_INTERVAL)
                    {
                        return;
                    }
                }
                lastDeviceDetectTime = now;

                AppendText("裝置插入或拔出");
                StartSearchDevice();
            });

            detector.DeviceRemoved += new EventHandler<DriveDetector.DriveDectctorEventArgs>((obj, ee) =>
            {
                long now = (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000;
                if (lastDeviceDetectTime > 0)
                {
                    if (now - lastDeviceDetectTime < DEVICE_DETECT_MIN_INTERVAL)
                    {
                        return;
                    }
                }
                lastDeviceDetectTime = now;

                AppendText("裝置插入或拔出");
                StartSearchDevice();
            });
        }
               
        private void FrmClient_FormClosed(object sender, FormClosedEventArgs e)
        {
            // 退出時關閉adb
            Process[] adbList = Process.GetProcessesByName("adb");
            foreach (Process adb in adbList)
            {
                adb.Kill();
                adb.Close();
                adb.Dispose();
            }
        }
        
        /// <summary>
        /// 接收到資料(阻塞線程)
        /// </summary>
        /// <param name="data"></param>
        private void CmdHelp_EventReceiveData(string data)
        {
            AppendTextInvoke(data);
        }

        /// <summary>
        /// 接收錯誤資料
        /// </summary>
        /// <param name="data"></param>
        private void CmdHelp_EventReceiveThreadErrorData(string data)
        {
            AppendTextInvoke(data);
        }

        /// <summary>
        /// 從線程接收到資料(不阻塞線程)
        /// </summary>
        /// <param name="data"></param>
        private void CmdHelp_EventReceiveThreadData(string data)
        {
            AppendTextInvoke(data);
        }
        
        /// <summary>
        /// 接收系統的消息(可以捕獲裝置連接配接斷開的消息)
        /// </summary>
        /// <param name="m"></param>
        protected override void WndProc(ref Message m)
        {
            bool habled = false;
            detector.WndProc(m.HWnd, m.Msg, m.WParam, m.LParam, ref habled);
            base.WndProc(ref m);
        }

        /// <summary>
        /// 開線程背景進行檢索
        /// </summary>
        private void StartSearchDevice()
        {
            Thread searchThread = new Thread(new ThreadStart(() =>
            {
                //如果馬上搜尋裝置,可能搜尋不到
                Thread.Sleep(1000);
                SearchDevice();
            }));
            searchThread.Start();
        }

        /// <summary>
        /// 通過ADB查找裝置
        /// 一般隻在開啟ADB成功之後,以及電腦檢測到裝置接入的時候運作
        /// </summary>
        private void SearchDevice()
        {            
            lock (m_lock)
            {
                string[] deviceInfoList = cmdHelp.GetDevices(CmdAdbInfo.adb_devices);
                // 一次插拔可能會觸發多次事件,隻在第一次時查找裝置,
                // 但第一次時查找可能查不到,是以等待一下後再次查詢
                if (deviceInfoList == null || deviceInfoList.Length < 1)
                {
                    Thread.Sleep(500);
                    deviceInfoList = cmdHelp.GetDevices(CmdAdbInfo.adb_devices);
                }
                this.Invoke(new Action<string[]>(RefreshTree), new object[1] { deviceInfoList });
            }
        }

        /// <summary>
        /// 重新整理裝置清單
        /// </summary>
        /// <param name="deviceInfoList"></param>
        private void RefreshTree(string[] deviceInfoList)
        {
            // 是否有新插入的裝置
            for (int i = 0; i < deviceInfoList.Length; i++)
            {
                string deviceNo = deviceInfoList[i];
                if (devices.Contains(deviceNo) == false)
                {
                    treeDevice.Nodes[0].Nodes.Add(cmdHelp.GetDeviceIMEI(deviceNo), deviceNo);
                    devices.Add(deviceNo);

                    // 新插入裝置,開始工作
                    int port = randomPort.Next(10000, 20000);
                    while (localPorts.Contains(port))
                    {
                        port = randomPort.Next(10000, 20000);
                    }
                    localPorts.Add(port);
                    StartWork(deviceNo, port.ToString());
                }
            }

            // 是否有拔出的裝置
            for (int i = devices.Count-1; i >= 0; i--)
            {
                if (deviceInfoList.Contains(devices[i]) == false)
                {
                    treeDevice.Nodes[0].Nodes.RemoveAt(i);

                    // 移除裝置,停止工作
                    StopWork(devices[i]);

                    devices.RemoveAt(i);                    
                }
            }

            treeDevice.ExpandAll();
        }

        /// <summary>
        /// 開始工作
        /// 1.檢查軟體是否有安裝,若有安裝,則擷取版本
        /// 2.檢查是否為新版本,不是的話,安裝新版本
        /// 3.啟動軟體
        /// 4.建立socket,請求資料
        /// 5.關閉軟體
        /// </summary>
        /// <param name="deviceNo"></param>
        private void StartWork(string deviceNo, string localPort)
        {
            // 啟動軟體
            cmdHelp.RunApp(deviceNo, "com.zhd.phoneaid/com.zhd.phoneaid.MainActivity");

            // 端口映射
            cmdHelp.Forward(deviceNo, localPort, "10086");

            // 建立socket,請求資料
            BackgroundWorker worker = new BackgroundWorker();
            worker.WorkerReportsProgress = false;
            worker.DoWork += Worker_DoWork;
            worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
            worker.RunWorkerAsync(localPort);
        }

        private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // 退出軟體
            //cmdHelp.ExitApp(deviceNo, "com.zhd.phoneaid");
        }

        private void Worker_DoWork(object sender, DoWorkEventArgs e)
        {
            SocketClient socketClient = new SocketClient(e.Argument.ToString());
            string result = socketClient.Request("upload file1");
            AppendTextInvoke(result);
            Thread.Sleep(10000);
            result = socketClient.Request("upload file2");
            AppendTextInvoke(result);
            Thread.Sleep(100);
            // 通知Android服務端退出
            socketClient.Request("quit");

            localPorts.Remove(int.Parse(socketClient.GetPort()));
        }

        private void StopWork(string deviceNo)
        {
            AppendText(deviceNo + " : 已拔出");
        }

        /// <summary>
        /// 輸出消息
        /// </summary>
        /// <param name="data"></param>
        private void AppendText(string data)
        {
            if (string.IsNullOrEmpty(data))
            {
                return;
            }
            if (data.EndsWith("\r\n") == false)
            {
                data = data + "\r\n";
            }
            txtMessage.AppendText(data);
        }

        /// <summary>
        /// 輸出消息(子線程調用)
        /// </summary>
        /// <param name="data"></param>
        private void AppendTextInvoke(string data)
        {
            this.Invoke(new Action<string>(AppendText), new object[1] { data });
        }
    }
}
           

SocketClient.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace PhoneAidClient
{
    public class SocketClient
    {
        /// <summary>
        /// 用戶端端口
        /// </summary>
        private string localPort;
        public string GetPort()
        {
            return localPort;
        }

        public SocketClient(string local_port)
        {
            this.localPort = local_port;
        }

        /// <summary>
        /// 發送請求
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public string Request(string msg)
        {
            Socket client = null;
            try
            {
                // 建立用戶端
                client = Create();
                // 連接配接到服務端
                Connect(client, localPort);
                if (client.Connected)
                {
                    // 發送請求
                    Send(client, msg);
                    // 接收應答
                    return Receive(client);
                }
                else
                {
                    return "連接配接失敗";
                }
            }
            catch (Exception e)
            {
                return $"Error:{e.Message}";
            }
            finally
            {
                // 斷開連接配接
                if (client.Connected)
                {
                    client.Shutdown(SocketShutdown.Both);
                    client?.Close();
                }
                client = null;
            }
        }
        
        /// <summary>
        /// 建立Socket用戶端
        /// </summary>
        /// <returns></returns>
        private static Socket Create()
        {
            return new Socket(AddressFamily.InterNetwork,
                SocketType.Stream,
                ProtocolType.Tcp);
        }

        /// <summary>
        /// 連接配接到服務端
        /// </summary>
        /// <param name="socket"></param>
        /// <param name="port"></param>
        private static void Connect(Socket socket, string port)
        {
            IPAddress iPAddress = IPAddress.Parse("127.0.0.1");
            IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, int.Parse(port));
            socket.Connect(iPEndPoint);
        }

        /// <summary>
        /// 發送請求
        /// </summary>
        /// <param name="socket"></param>
        /// <param name="msg"></param>
        private static void Send(Socket socket, string msg)
        {
            msg += "\n";
            byte[] data = Encoding.UTF8.GetBytes(msg);
            socket.Send(data);
        }

        /// <summary>
        /// 接收應答
        /// </summary>
        /// <param name="socket"></param>
        /// <returns></returns>
        private static string Receive(Socket socket)
        {
            string str = "";
            byte[] data = new byte[1024];
            int len = 0;
            int i = 0;
            while ((i = socket.Receive(data, 1024, SocketFlags.None)) != 0)
            {
                len += i;
                string piece = Encoding.UTF8.GetString(data, 0, i);
                str += piece;
            }
            return str;
        }
    }
}
           

DriverDetector.cs

using System;
using System.Runtime.InteropServices;

namespace PhoneAidClient
{
    /// <summary>
    /// 監聽裝置的插入和拔出
    /// </summary>
    public class DriveDetector
    {
        /// <summary>
        /// 裝置插入事件
        /// </summary>
        public event EventHandler<DriveDectctorEventArgs> DeviceArrived = null;

        /// <summary>
        /// 裝置拔出事件
        /// </summary>
        public event EventHandler<DriveDectctorEventArgs> DeviceRemoved = null;

        /// <summary>
        /// 消息處理(HwndSourceHook委托的簽名)
        /// </summary>
        /// <param name="hwnd"></param>
        /// <param name="msg"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <param name="handled"></param>
        /// <returns></returns>
        public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == NativeConstants.WM_DEVICECHANGE)
            {
                switch (wParam.ToInt32())
                {
                    case NativeConstants.DBT_DEVICEARRIVAL:
                        {
                            var devType = Marshal.ReadInt32(lParam, 4);
                            if (devType == NativeConstants.DBT_DEVTYP_VOLUME)
                            {
                                if (DeviceArrived != null)
                                {
                                    DeviceArrived(this, null); //觸發裝置插入事件
                                }
                            }
                        }
                        break;
                    case NativeConstants.DBT_DEVICEREMOVECOMPLETE:
                        {
                            var devType = Marshal.ReadInt32(lParam, 4);
                            if (devType == NativeConstants.DBT_DEVTYP_VOLUME)
                            {
                                if (DeviceRemoved != null)
                                {
                                    DeviceRemoved(this, null);
                                }
                            }
                        }
                        break;
                    //64位win10系統下,裝置連接配接斷開都發送一樣的指令
                    case NativeConstants.DBT_DEVICEARRIVAL64:
                        DeviceArrived(this, null); //觸發裝置插入事件
                        break;
                }
            }
            return IntPtr.Zero;
        }

        /// <summary>
        /// 裝置插入或拔出事件
        /// </summary>
        public class DriveDectctorEventArgs : EventArgs
        {
            /// <summary>
            /// 獲得裝置卷标
            /// </summary>
            public string Drive { get; private set; }
                public DriveDectctorEventArgs(string drive)
                {
                    Drive = drive ?? string.Empty;
                }
            }
        
        public partial class NativeConstants
        {
            /// WM_DEVICECHANGE -> 0x0219
            public const int WM_DEVICECHANGE = 537;
            /// BROADCAST_QUERY_DENY -> 0x424D5144
            //public const int BROADCAST_QUERY_DENY = 1112363332;
            //public const int DBT_DEVTYP_DEVICEINTERFACE = 5;
            //public const int DBT_DEVTYP_HANDLE = 6;
            public const int DBT_DEVICEARRIVAL = 0x8000; // system detected a new device
            //public const int DBT_DEVICEQUERYREMOVE = 0x8001;   // Preparing to remove (any program can disable the removal)
            public const int DBT_DEVICEREMOVECOMPLETE = 0x8004; // removed 
            public const int DBT_DEVTYP_VOLUME = 0x00000002; // drive type is logical volume
            public const int DBT_DEVICEARRIVAL64 = 0x00000007;
        }
    }
}
           

ADB操作

FrmClient中使用的CmdHelp用于執行ADB指令,涉及很多沒有使用到的方法,就不附上了,下面隻列出使用到的ADB指令。

  • 啟動adb服務

    start-server

  • 擷取裝置清單

    devices

  • 擷取指定裝置的IMEI

    -s {deviceName} shell getprop gsm.mtk.imei1

  • 啟動app

    -s {deviceName} shell am start -n {package/package.activity}

  • 端口映射

    -s {deviceName} forward tcp:{client port} tcp:{server port}

運作效果

Android服務端

無網絡PC通過USB與多個Android裝置通訊

PC用戶端

無網絡PC通過USB與多個Android裝置通訊

參考資料

  • PC通過USB連接配接Android通信(Socket)
  • Android通過USB與PC通信