参考资料:
由于客户应用使用的场景比较特殊。明确要求不能使用网络进行数据交互,所以不得不研究了一下使用usb通信这方面。原理就是当连上usb线后,通过socket进行数据通信。只不过android设备作为socket服务端,pc作为socket的客户端。pc端在与服务端建立连接之前需要使用adb命令设置转发端口(具体可参考参考资料1和参考资料2)。
端口这玩意随便填,不跟别人冲突就行。
adb shell am broadcast -a NotifyServiceStop
adb forward tcp:9999 tcp:9000
adb shell am broadcast -a NotifyServiceStart
##使用代码调用命令行
如果想要完善android通过usb先进行数据交互,这里应该有不少的命令能用到,这里先记录一下调用命令的基本使用。
import java.io.BufferedReader
import java.io.InputStreamReader
fun main() {
//读取连接设备
val process = Runtime.getRuntime().exec("adb devices")
val devices = BufferedReader(InputStreamReader(process.inputStream))
val stringBuilder = StringBuilder()
var line: String? = null
while (devices.readLine().apply { line = this } != null) {
stringBuilder.append("$line\n")
}
println(stringBuilder.toString())
//读取安装的应用
val process2 = Runtime.getRuntime().exec("adb shell pm list packages")
val packages = BufferedReader(InputStreamReader(process2.inputStream))
val sb = StringBuilder()
var line2: String? = null
while (packages.readLine().apply { line2 = this } != null) {
sb.append("$line2\n")
}
println(sb.toString())
}
打印结果如下图所示:
上面运行结果不错就是代码量有点多,用python简化一下就清晰不少。
import subprocess
if __name__ == '__main__':
# 读取连接设备
subprocess.call("adb devices", shell=True)
# 读取安装的应用
subprocess.call("adb shell pm list packages", shell=True)
Android端作为Socket的服务端,用来接收文件。为了防止分包的问题这里我定义了一个封包和解包方式。
在文件传输的时候,数据的收发都会以这种结构去发送或者接收。(实际情况下肯定跟这个不一样,我这为了方便携带参数用的就是json格式的数据)。理论上就是每次读取的字节大小都在携带信息中。如果在循环读取中有一次没有读满,那就把它应当读取的字节都读取出来,再循环下一次数据读取,省着出现粘包的现象。(byte也别设太大,要不内存溢出)
package com.lyan.usbtestphone
import android.annotation.SuppressLint
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import com.blankj.utilcode.constant.PermissionConstants
import com.blankj.utilcode.util.GsonUtils
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.PermissionUtils
import com.blankj.utilcode.util.ToastUtils
import kotlinx.android.synthetic.main.activity_main.*
import java.io.*
import java.net.ServerSocket
import java.net.Socket
class MainActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
private val handler = Handler(Handler.Callback {
when (it.what) {
1 -> progressTv.text = "${it.obj}%"
}
false
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testBtn.setOnClickListener {
startSocketServer { LogUtils.i("${Thread.currentThread().name} : $it") }
}
}
private fun startSocketServer(callback: (name: Socket) -> Unit) = Thread {
val file = File(Environment.getExternalStorageDirectory(), "copy.db3")
if (file.exists()) {
if (file.delete()) {
file.createNewFile()
}
} else {
file.createNewFile()
}
ServerSocket(9000).apply {
logI("移动服务端等待客户端的连接...")
val client = this.accept()
callback.invoke(client)
val bufferedOutputStream = BufferedOutputStream(DataOutputStream(FileOutputStream(file)))
val bufferedInputStream = BufferedInputStream(DataInputStream(client.getInputStream()))
val tagBytes = ByteArray(6)
val infoBytes = ByteArray(4)
var hTag: String//标记头
var fTag: String//标记尾
var infoSize: Int//携带数据的一些信息
var jsonBytes: ByteArray//携带数据的byte数组
var sendInfoData: SendInfoData//解析后的携带信息
var readBytes: ByteArray//真正传输的数据的byte数组
var readSize: Int//真正传输的数据的byte长度
while (true) {
val len = bufferedInputStream.read(tagBytes)
if (len == -1) {
break
}
hTag = String(tagBytes, 0, len)//标记头
//读取进度信息
infoSize = bufferedInputStream.read(infoBytes).run { bytesToInt(infoBytes) }
jsonBytes = ByteArray(infoSize)
sendInfoData = bufferedInputStream.read(jsonBytes).run {
val infoJson = when (infoSize) {
this -> {//读取数据完整
LogUtils.i("读取数据完整")
String(jsonBytes, 0, this)
}
else -> {//读取数据不完整(此处只要将分包处理后、粘包的问题自然就不会出现了)
LogUtils.i("读取数据不完整")
bufferedInputStream.read(jsonBytes, this, infoSize - this).run {
String(jsonBytes, 0, infoSize)
}
}
}
LogUtils.i("其他信息 ------> $infoJson")
GsonUtils.getGson().fromJson(infoJson, SendInfoData::class.java)
}
handler.obtainMessage(1, sendInfoData.percent).sendToTarget()
//读取数据信息 传输数据大小 解析数据长度
readBytes = ByteArray(sendInfoData.sendSize)
readSize = bufferedInputStream.read(readBytes)//已读数据大小
LogUtils.i("读流的长度:$readSize")
if (readSize < sendInfoData.sendSize) {
LogUtils.w("读取数据不完整!已读数据少于应读取数据的大小……",
"应读:${sendInfoData.sendSize}", "实读:$readSize")
bufferedInputStream.read(readBytes, readSize, sendInfoData.sendSize - readSize)
}
fTag = bufferedInputStream.read(tagBytes).run { String(tagBytes, 0, this) }
LogUtils.w("header:$hTag", "携带信息的字节数:$infoSize", "携带信息内容:$sendInfoData",
"每次应读取的数据字节数量:${sendInfoData.sendSize}", "footer:$fTag")
bufferedOutputStream.write(readBytes)
bufferedOutputStream.flush()
}
}
}.start()
//请求权限(文件读写权限)
override fun onResume() {
super.onResume()
PermissionUtils.permission(PermissionConstants.STORAGE).callback(object : PermissionUtils.SimpleCallback {
override fun onGranted() {
ToastUtils.showShort("获取文件读写权限成功!")
}
override fun onDenied() {
}
}).request()
}
private fun bytesToInt(bytes: ByteArray): Int {
return (0 until bytes.size).sumBy { (bytes[it].toInt() and 0xFF) shl it * 8 }
}
//每次接收数据的信息 进度 和 要保存的数据字节大小
data class SendInfoData(val percent: String, val sendSize: Int)
}
在这种情况下后台就成客户端了。这里为了减少socket客户端的代码量,所以使用ptyhon来写:
import subprocess
import socket
import time
import copy
import json
import os
class SendInfoData(object):
def __init__(self, percent="", sendSize=0):
self.__percent = percent
self.__sendSize = sendSize
def toJson(self):
return json.dumps({
"percent": self.__percent,
"sendSize": self.__sendSize,
})
# int转byte数组
def intToBytes(intNumber=0): return intNumber.to_bytes(4, "little")
# str转utf-8 byte数组
def strToUtf8Bytes(value): return bytes(value, encoding="utf-8")
if __name__ == '__main__':
subprocess.call("adb shell am broadcast -a NotifyServiceStop", shell=True)
subprocess.call("adb forward tcp:9999 tcp:9000", shell=True)
subprocess.call("adb shell am broadcast -a NotifyServiceStart", shell=True)
client = socket.socket()
result = client.connect(("127.0.0.1", 9999))
# 文件路径
filePath = "/Users/apple/Downloads/base.db3"
# 文件大小
allSize = os.path.getsize(filePath)
fileSize = copy.deepcopy(allSize)
print("%s\n" % fileSize)
defaultReadSize = 1024 * 10
progress = 0
h = time.time()
# 读取 文件
with open(filePath, mode="rb") as readFile:
while True:
if fileSize <= 0: break
readSize = defaultReadSize if fileSize > defaultReadSize else fileSize
progress += readSize
percent = '{: .2f}'.format(progress * 1.0 / allSize * 100)
print("进度:%s" % percent)
# 读取内容
readBytes = readFile.read(readSize)
if not readBytes: break
tagH = strToUtf8Bytes("@tag:h")
tagF = strToUtf8Bytes("@tag:f")
# 携带信息
infoJson = SendInfoData(percent, readSize).toJson()
print("json:%s\n" % infoJson)
infoBytes = strToUtf8Bytes(infoJson)
infoSize = intToBytes(len(infoBytes))
# 包裹传输数据
client.send(tagH) # 标记开头
client.send(infoSize) # 携带参数byte数据长度
client.send(infoBytes) # 携带参数内容
client.send(readBytes) # 真实传输的内容
client.send(tagF) # 标记结尾
fileSize -= readSize
client.close()
f = time.time()
print("用时:{: .0f}s".format((f - h)))
这个例子的界面比较简单,就一个按钮和一个文本。手上测试的文件是一个900多兆的文件,在单位的时候试过一个4个多G的sqlite文件。而且文件在传输后一样可以正常使用。说明这个方式还是可行的。(当然在实际项目中这个例子仅仅是证明这个方式可行而已,具体优化部分肯定不带少的)
目的是验证,这里以读取文件的内容为例,文件是txt格式的这里放了一段字符串“一二三四五六七八九十”。一共是10字符(一个字符2个字节,也就是20个字节)。定义一个byte数组长度为20。先读一半,然后再读剩下的一半。
import java.io.File
import java.io.FileInputStream
import java.nio.charset.Charset
fun main() {
val path = "/Users/apple/Downloads/O.txt"
val file = File(path)
val inputStream = FileInputStream(file)
//available()这个方法本质的意义是 剩余未被读取的字节数量
println("文件的字节总数:${inputStream.available()}")
val byte = ByteArray(20)
val read1 = inputStream.read(byte, 0, 10)
println(read1)
println("read1后剩余字节数量:${inputStream.available()}")
val read1Msg = String(byte, 0, read1, Charset.forName("GBk"))
println(read1Msg)
inputStream.read(byte, 10, 20 - read1)
println("read2后剩余字节数量:${inputStream.available()}")
val read2Msg = String(byte, 0, byte.size, Charset.forName("GBk"))
println(read2Msg)
}