增加文件传输功能
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
#define LOG_TAG "ReceiverEngine"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
ReceiverEngine::ReceiverEngine(JNIEnv* env, jobject surface) {
|
||||
ReceiverEngine::ReceiverEngine(JNIEnv* env, jobject surface, jobject callbackObj) {
|
||||
env->GetJavaVM(&jvm_);
|
||||
callbackObj_ = env->NewGlobalRef(callbackObj);
|
||||
window_ = ANativeWindow_fromSurface(env, surface);
|
||||
|
||||
// Bind callback
|
||||
receiver_.SetCallback([this](const std::vector<uint8_t>& data, const FrameHeader& header) {
|
||||
this->OnFrameReceived(data, header);
|
||||
});
|
||||
|
||||
receiver_.SetSenderDetectedCallback([this](const std::string& ip, int port) {
|
||||
this->NotifyJavaSenderDetected(ip, port);
|
||||
});
|
||||
}
|
||||
|
||||
ReceiverEngine::~ReceiverEngine() {
|
||||
@@ -19,8 +25,43 @@ ReceiverEngine::~ReceiverEngine() {
|
||||
ANativeWindow_release(window_);
|
||||
window_ = nullptr;
|
||||
}
|
||||
if (callbackObj_ && jvm_) {
|
||||
JNIEnv* env = nullptr;
|
||||
if (jvm_->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) {
|
||||
env->DeleteGlobalRef(callbackObj_);
|
||||
} else {
|
||||
// If thread not attached, we leak? Or attach to delete?
|
||||
// Usually destructor called on main thread or known thread.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReceiverEngine::NotifyJavaSenderDetected(const std::string& ip, int port) {
|
||||
if (!jvm_ || !callbackObj_) return;
|
||||
|
||||
JNIEnv* env = nullptr;
|
||||
bool needsDetach = false;
|
||||
int res = jvm_->GetEnv((void**)&env, JNI_VERSION_1_6);
|
||||
if (res == JNI_EDETACHED) {
|
||||
if (jvm_->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
|
||||
needsDetach = true;
|
||||
}
|
||||
|
||||
jclass clazz = env->GetObjectClass(callbackObj_);
|
||||
jmethodID methodId = env->GetMethodID(clazz, "onSenderDetected", "(Ljava/lang/String;I)V");
|
||||
if (methodId) {
|
||||
jstring jIp = env->NewStringUTF(ip.c_str());
|
||||
env->CallVoidMethod(callbackObj_, methodId, jIp, port);
|
||||
env->DeleteLocalRef(jIp);
|
||||
}
|
||||
|
||||
if (needsDetach) {
|
||||
jvm_->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ReceiverEngine::Start(int port) {
|
||||
receiver_.Start(port);
|
||||
}
|
||||
|
||||
@@ -8,17 +8,25 @@
|
||||
|
||||
class ReceiverEngine {
|
||||
public:
|
||||
ReceiverEngine(JNIEnv* env, jobject surface);
|
||||
ReceiverEngine(JNIEnv* env, jobject surface, jobject callbackObj);
|
||||
~ReceiverEngine();
|
||||
|
||||
void Start(int port);
|
||||
void Stop();
|
||||
|
||||
// Callback to JNI
|
||||
void SetSenderDetectedCallback(std::function<void(const std::string&, int)> cb);
|
||||
|
||||
private:
|
||||
void OnFrameReceived(const std::vector<uint8_t>& data, const FrameHeader& header);
|
||||
void NotifyJavaSenderDetected(const std::string& ip, int port);
|
||||
|
||||
ANativeWindow* window_ = nullptr;
|
||||
UdpReceiver receiver_;
|
||||
VideoDecoder decoder_;
|
||||
bool decoder_initialized_ = false;
|
||||
std::function<void(const std::string&, int)> sender_cb_;
|
||||
|
||||
JavaVM* jvm_ = nullptr;
|
||||
jobject callbackObj_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "UdpReceiver.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
@@ -56,17 +57,36 @@ void UdpReceiver::SetCallback(OnFrameReceivedCallback callback) {
|
||||
callback_ = callback;
|
||||
}
|
||||
|
||||
void UdpReceiver::SetSenderDetectedCallback(OnSenderDetectedCallback callback) {
|
||||
sender_callback_ = callback;
|
||||
}
|
||||
|
||||
void UdpReceiver::ReceiveLoop() {
|
||||
std::vector<uint8_t> buffer(65535);
|
||||
LOGI("Receiver thread started");
|
||||
|
||||
while (running_) {
|
||||
ssize_t received = recvfrom(sockfd_, buffer.data(), buffer.size(), 0, nullptr, nullptr);
|
||||
struct sockaddr_in senderAddr;
|
||||
socklen_t senderLen = sizeof(senderAddr);
|
||||
ssize_t received = recvfrom(sockfd_, buffer.data(), buffer.size(), 0, (struct sockaddr*)&senderAddr, &senderLen);
|
||||
if (received <= 0) {
|
||||
if (running_) LOGE("Recv failed or socket closed");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check sender IP
|
||||
char ipStr[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &(senderAddr.sin_addr), ipStr, INET_ADDRSTRLEN);
|
||||
std::string senderIp(ipStr);
|
||||
|
||||
if (senderIp != last_sender_ip_) {
|
||||
last_sender_ip_ = senderIp;
|
||||
LOGI("New sender detected: %s", senderIp.c_str());
|
||||
if (sender_callback_) {
|
||||
sender_callback_(senderIp, 8889); // Assume TCP port 8889
|
||||
}
|
||||
}
|
||||
|
||||
if (received < 8) continue; // Header size check
|
||||
|
||||
// Parse Packet Header
|
||||
|
||||
@@ -17,6 +17,7 @@ struct FrameHeader {
|
||||
|
||||
// Callback for full frames
|
||||
using OnFrameReceivedCallback = std::function<void(const std::vector<uint8_t>& frameData, const FrameHeader& header)>;
|
||||
using OnSenderDetectedCallback = std::function<void(const std::string& ip, int port)>;
|
||||
|
||||
class UdpReceiver {
|
||||
public:
|
||||
@@ -26,6 +27,7 @@ public:
|
||||
bool Start(int port);
|
||||
void Stop();
|
||||
void SetCallback(OnFrameReceivedCallback callback);
|
||||
void SetSenderDetectedCallback(OnSenderDetectedCallback callback);
|
||||
|
||||
private:
|
||||
void ReceiveLoop();
|
||||
@@ -34,6 +36,8 @@ private:
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread worker_thread_;
|
||||
OnFrameReceivedCallback callback_;
|
||||
OnSenderDetectedCallback sender_callback_;
|
||||
std::string last_sender_ip_;
|
||||
|
||||
// Fragmentation handling
|
||||
struct Fragment {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_com_displayflow_receiver_MainActivity_nativeInit(
|
||||
JNIEnv* env,
|
||||
jobject /* this */,
|
||||
jobject thiz,
|
||||
jobject surface) {
|
||||
auto engine = new ReceiverEngine(env, surface);
|
||||
auto engine = new ReceiverEngine(env, surface, thiz);
|
||||
return reinterpret_cast<jlong>(engine);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,35 +7,65 @@ import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.graphics.Color
|
||||
import android.widget.Button
|
||||
|
||||
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
|
||||
private lateinit var surfaceView: SurfaceView
|
||||
private var nativeEngine: Long = 0
|
||||
private lateinit var fileTransfer: TcpFileTransfer
|
||||
private val REQUEST_CODE_PICK_FILE = 1001
|
||||
private val REQUEST_CODE_PICK_FOLDER = 1002
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Keep screen on
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
// Create layout programmatically
|
||||
val layout = FrameLayout(this)
|
||||
surfaceView = SurfaceView(this)
|
||||
layout.addView(surfaceView)
|
||||
val btn = Button(this)
|
||||
btn.text = "发送文件夹"
|
||||
btn.textSize = 12f
|
||||
btn.setTextColor(Color.WHITE)
|
||||
btn.setBackgroundColor(0x66000000)
|
||||
btn.alpha = 0.8f
|
||||
btn.setPadding(24, 12, 24, 12)
|
||||
val margin = (16 * resources.displayMetrics.density).toInt()
|
||||
val lp = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.END or Gravity.BOTTOM
|
||||
)
|
||||
lp.setMargins(margin, margin, margin, margin)
|
||||
layout.addView(btn, lp)
|
||||
setContentView(layout)
|
||||
|
||||
surfaceView.holder.addCallback(this)
|
||||
|
||||
fileTransfer = TcpFileTransfer(this)
|
||||
|
||||
surfaceView.setOnLongClickListener {
|
||||
pickFileToSend()
|
||||
true
|
||||
}
|
||||
btn.setOnClickListener {
|
||||
pickFolderToSend()
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
// Init native engine with surface
|
||||
nativeEngine = nativeInit(holder.surface)
|
||||
nativeStart(nativeEngine, 8888) // Listen on port 8888
|
||||
nativeStart(nativeEngine, 8888)
|
||||
}
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
// Handle resize if needed
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
@@ -46,6 +76,38 @@ class MainActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
fun onSenderDetected(ip: String, port: Int) {
|
||||
Log.i("MainActivity", "Sender detected: $ip:$port")
|
||||
fileTransfer.connect(ip, port)
|
||||
}
|
||||
|
||||
private fun pickFileToSend() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
startActivityForResult(Intent.createChooser(intent, "选择要发送的文件"), REQUEST_CODE_PICK_FILE)
|
||||
}
|
||||
private fun pickFolderToSend() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_FOLDER)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
|
||||
val uri: Uri? = data?.data
|
||||
if (uri != null) {
|
||||
fileTransfer.sendFile(uri)
|
||||
}
|
||||
}
|
||||
if (requestCode == REQUEST_CODE_PICK_FOLDER && resultCode == RESULT_OK) {
|
||||
val uri: Uri? = data?.data
|
||||
if (uri != null) {
|
||||
fileTransfer.sendFolder(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Native methods
|
||||
external fun nativeInit(surface: Surface): Long
|
||||
external fun nativeStart(enginePtr: Long, port: Int)
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.displayflow.receiver
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContentValues
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.provider.MediaStore
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.net.Socket
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class TcpFileTransfer(private val context: Context) {
|
||||
private var socket: Socket? = null
|
||||
private var dis: DataInputStream? = null
|
||||
private var dos: DataOutputStream? = null
|
||||
private var isConnected = false
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun connect(ip: String, port: Int) {
|
||||
Thread {
|
||||
try {
|
||||
if (isConnected) return@Thread
|
||||
Log.i("TcpFileTransfer", "Connecting to $ip:$port")
|
||||
socket = Socket(ip, port)
|
||||
dis = DataInputStream(socket!!.getInputStream())
|
||||
dos = DataOutputStream(socket!!.getOutputStream())
|
||||
isConnected = true
|
||||
Log.i("TcpFileTransfer", "Connected")
|
||||
|
||||
// Start receiving loop
|
||||
receiveLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.e("TcpFileTransfer", "Connection failed", e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun receiveLoop() {
|
||||
try {
|
||||
while (isConnected) {
|
||||
val type = dis!!.readUnsignedByte()
|
||||
val sizeBytes = ByteArray(4)
|
||||
dis!!.readFully(sizeBytes)
|
||||
val realSize = java.nio.ByteBuffer.wrap(sizeBytes).order(java.nio.ByteOrder.LITTLE_ENDIAN).int
|
||||
|
||||
val payload = ByteArray(realSize)
|
||||
if (realSize > 0) {
|
||||
dis!!.readFully(payload)
|
||||
}
|
||||
|
||||
when (type) {
|
||||
0x10 -> handleFileHeader(payload)
|
||||
0x11 -> handleFileData(payload)
|
||||
0x12 -> handleFileEnd()
|
||||
0x20 -> handleFolderHeader(payload)
|
||||
0x21 -> handleDirEntry(payload)
|
||||
0x22 -> handleFileHeaderV2(payload)
|
||||
0x23 -> { folderRootName = null }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("TcpFileTransfer", "Receive loop error", e)
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private var currentFileOutputStream: OutputStream? = null
|
||||
private var currentFileName: String = ""
|
||||
private var currentFileSize: Long = 0
|
||||
private var currentFilePath: File? = null
|
||||
private var currentFileUri: Uri? = null
|
||||
private var folderRootName: String? = null
|
||||
|
||||
private fun handleFileHeader(payload: ByteArray) {
|
||||
val buffer = java.nio.ByteBuffer.wrap(payload)
|
||||
buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
currentFileSize = buffer.long
|
||||
val nameBytes = ByteArray(256)
|
||||
buffer.get(nameBytes)
|
||||
|
||||
val nameString = String(nameBytes, StandardCharsets.UTF_8).trim { it <= ' ' }
|
||||
currentFileName = nameString.substringBefore('\u0000')
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, currentFileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
|
||||
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
if (uri != null) {
|
||||
currentFileOutputStream = context.contentResolver.openOutputStream(uri)
|
||||
currentFileUri = uri
|
||||
Log.i("TcpFileTransfer", "Receiving file to public Downloads (MediaStore): $currentFileName ($currentFileSize bytes)")
|
||||
} else {
|
||||
Log.e("TcpFileTransfer", "Failed to create MediaStore entry for $currentFileName")
|
||||
}
|
||||
} else {
|
||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(downloadsDir, currentFileName)
|
||||
Log.i("TcpFileTransfer", "Receiving file to public Downloads: ${file.absolutePath} ($currentFileSize bytes)")
|
||||
currentFileOutputStream = file.outputStream()
|
||||
currentFilePath = file
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFileData(payload: ByteArray) {
|
||||
currentFileOutputStream?.write(payload)
|
||||
}
|
||||
|
||||
private fun handleFileEnd() {
|
||||
currentFileOutputStream?.close()
|
||||
currentFileOutputStream = null
|
||||
Log.i("TcpFileTransfer", "File received successfully")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
currentFileUri?.let {
|
||||
mainHandler.post { Toast.makeText(context, "已保存到 下载: $currentFileName", Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
} else {
|
||||
currentFilePath?.let {
|
||||
mainHandler.post { Toast.makeText(context, "已保存: ${it.absolutePath}", Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleFolderHeader(payload: ByteArray) {
|
||||
val name = String(payload, StandardCharsets.UTF_8).substringBefore('\u0000')
|
||||
folderRootName = if (name.isNotBlank()) name else "AndroidFolder"
|
||||
}
|
||||
private fun handleDirEntry(payload: ByteArray) {
|
||||
val rel = String(payload, StandardCharsets.UTF_8).substringBefore('\u0000').replace('\\', '/')
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
val base = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val dir = File(base, (folderRootName ?: "AndroidFolder") + "/" + rel)
|
||||
dir.mkdirs()
|
||||
}
|
||||
}
|
||||
private fun handleFileHeaderV2(payload: ByteArray) {
|
||||
val buf = java.nio.ByteBuffer.wrap(payload).order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
val size = buf.long
|
||||
val pathLen = buf.short.toInt() and 0xFFFF
|
||||
val pathBytes = ByteArray(pathLen)
|
||||
buf.get(pathBytes)
|
||||
val rel = String(pathBytes, StandardCharsets.UTF_8).replace('\\', '/')
|
||||
currentFileSize = size
|
||||
val lastSlash = rel.lastIndexOf('/')
|
||||
val parent = if (lastSlash >= 0) rel.substring(0, lastSlash) else ""
|
||||
val fileName = if (lastSlash >= 0) rel.substring(lastSlash + 1) else rel
|
||||
currentFileName = fileName
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val relPath = if (parent.isNotBlank()) (Environment.DIRECTORY_DOWNLOADS + "/" + (folderRootName ?: "AndroidFolder") + "/" + parent)
|
||||
else (Environment.DIRECTORY_DOWNLOADS + "/" + (folderRootName ?: "AndroidFolder"))
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, currentFileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
|
||||
put(MediaStore.Downloads.RELATIVE_PATH, relPath)
|
||||
}
|
||||
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
if (uri != null) {
|
||||
currentFileOutputStream = context.contentResolver.openOutputStream(uri)
|
||||
currentFileUri = uri
|
||||
} else {
|
||||
currentFileOutputStream = null
|
||||
}
|
||||
} else {
|
||||
val base = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val dir = File(base, (folderRootName ?: "AndroidFolder") + (if (parent.isNotBlank()) "/$parent" else ""))
|
||||
dir.mkdirs()
|
||||
val f = File(dir, currentFileName)
|
||||
currentFileOutputStream = f.outputStream()
|
||||
currentFilePath = f
|
||||
}
|
||||
}
|
||||
|
||||
fun sendFile(uri: Uri) {
|
||||
Thread {
|
||||
try {
|
||||
if (!isConnected || dos == null) {
|
||||
Log.e("TcpFileTransfer", "Not connected")
|
||||
return@Thread
|
||||
}
|
||||
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
val nameIndex = cursor?.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
val sizeIndex = cursor?.getColumnIndex(android.provider.OpenableColumns.SIZE)
|
||||
|
||||
cursor?.moveToFirst()
|
||||
val fileName = cursor?.getString(nameIndex ?: 0) ?: "unknown_file"
|
||||
val fileSize = cursor?.getLong(sizeIndex ?: 0) ?: 0L
|
||||
cursor?.close()
|
||||
|
||||
val inputStream = context.contentResolver.openInputStream(uri) ?: return@Thread
|
||||
|
||||
dos!!.writeByte(0x10)
|
||||
dos!!.writeInt(Integer.reverseBytes(264))
|
||||
|
||||
val buffer = java.nio.ByteBuffer.allocate(264)
|
||||
buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
buffer.putLong(fileSize)
|
||||
val nameBytes = fileName.toByteArray(StandardCharsets.UTF_8)
|
||||
val finalNameBytes = ByteArray(256)
|
||||
System.arraycopy(nameBytes, 0, finalNameBytes, 0, Math.min(nameBytes.size, 256))
|
||||
buffer.put(finalNameBytes)
|
||||
|
||||
dos!!.write(buffer.array())
|
||||
|
||||
val chunk = ByteArray(64 * 1024)
|
||||
var bytesRead: Int
|
||||
while (inputStream.read(chunk).also { bytesRead = it } != -1) {
|
||||
dos!!.writeByte(0x11)
|
||||
dos!!.writeInt(Integer.reverseBytes(bytesRead))
|
||||
dos!!.write(chunk, 0, bytesRead)
|
||||
}
|
||||
inputStream.close()
|
||||
|
||||
dos!!.writeByte(0x12)
|
||||
dos!!.writeInt(0)
|
||||
|
||||
Log.i("TcpFileTransfer", "File sent successfully")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("TcpFileTransfer", "Send file failed", e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
fun sendFolder(uri: Uri) {
|
||||
Thread {
|
||||
try {
|
||||
if (!isConnected || dos == null) {
|
||||
Log.e("TcpFileTransfer", "Not connected")
|
||||
return@Thread
|
||||
}
|
||||
val root = DocumentFile.fromTreeUri(context, uri) ?: return@Thread
|
||||
val rootName = root.name ?: "AndroidFolder"
|
||||
val nameBytes = rootName.toByteArray(StandardCharsets.UTF_8)
|
||||
dos!!.writeByte(0x20)
|
||||
dos!!.writeInt(Integer.reverseBytes(nameBytes.size))
|
||||
dos!!.write(nameBytes)
|
||||
fun sendEntry(doc: DocumentFile, base: String) {
|
||||
val rel = if (base.isEmpty()) (doc.name ?: "") else (base + "/" + (doc.name ?: ""))
|
||||
if (doc.isDirectory) {
|
||||
val pathBytes = rel.toByteArray(StandardCharsets.UTF_8)
|
||||
dos!!.writeByte(0x21)
|
||||
dos!!.writeInt(Integer.reverseBytes(pathBytes.size))
|
||||
dos!!.write(pathBytes)
|
||||
doc.listFiles().forEach { child ->
|
||||
sendEntry(child, rel)
|
||||
}
|
||||
} else {
|
||||
val size = doc.length()
|
||||
val pathBytes = rel.toByteArray(StandardCharsets.UTF_8)
|
||||
val headerSize = 8 + 2 + pathBytes.size
|
||||
dos!!.writeByte(0x22)
|
||||
dos!!.writeInt(Integer.reverseBytes(headerSize))
|
||||
val bb = java.nio.ByteBuffer.allocate(headerSize)
|
||||
bb.order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
bb.putLong(size)
|
||||
bb.putShort(pathBytes.size.toShort())
|
||||
bb.put(pathBytes)
|
||||
dos!!.write(bb.array())
|
||||
val input = context.contentResolver.openInputStream(doc.uri)
|
||||
if (input != null) {
|
||||
val chunk = ByteArray(64 * 1024)
|
||||
var bytesRead: Int
|
||||
while (input.read(chunk).also { bytesRead = it } != -1) {
|
||||
dos!!.writeByte(0x11)
|
||||
dos!!.writeInt(Integer.reverseBytes(bytesRead))
|
||||
dos!!.write(chunk, 0, bytesRead)
|
||||
}
|
||||
input.close()
|
||||
}
|
||||
dos!!.writeByte(0x12)
|
||||
dos!!.writeInt(0)
|
||||
}
|
||||
}
|
||||
root.listFiles().forEach { sendEntry(it, "") }
|
||||
dos!!.writeByte(0x23)
|
||||
dos!!.writeInt(0)
|
||||
Log.i("TcpFileTransfer", "Folder sent successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("TcpFileTransfer", "Send folder failed", e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
isConnected = false
|
||||
try {
|
||||
socket?.close()
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user