a.0.1.0 move to creates src

This commit is contained in:
netadmin committed 2026-10-27 22:23:15 +03:00
1 parent 86d3a3b47f
commit f22d954105
942 files changed
+275635 -225

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="false"
android:label="@string/app_name"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.RamShare">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".RamshareService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,290 @@
package com.ramshare.app
import android.content.ContentResolver
import android.net.Uri
import android.provider.OpenableColumns
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
data class PeerDto(
val id: String,
val name: String,
val ips: List<String>,
val port: Int,
val fingerprint: String,
val pinCode: String,
val verified: Boolean,
val trustsMe: Boolean,
val online: Boolean,
val lastSeen: Long,
val lastPing: Long,
val failCount: Int
)
data class TextDto(
val sender: String,
val text: String,
val timestamp: Long
)
data class MediaDto(
val sender: String,
val filename: String,
val savedPath: String,
val mime: String,
val size: Long,
val timestamp: Long,
val previewable: Boolean,
val risky: Boolean
)
data class TransferDto(
val id: String,
val filename: String,
val peerName: String,
val direction: String,
val transferredBytes: Long,
val totalBytes: Long,
val percent: Int,
val done: Boolean,
val error: String?
)
data class ScanDto(
val active: Boolean,
val range: String,
val scanned: Int,
val total: Int,
val found: Int
)
data class NodeStateDto(
val deviceName: String = "Загрузка...",
val myIp: String = "127.0.0.1",
val p2pPort: Int = 8443,
val myFingerprint: String = "",
val myPin: String = "--------",
val mediaDir: String = "",
val pingIntervalSec: Int = 15,
val lastPing: Long = 0L,
val maxPingFails: Int = 5,
val now: Long = 0L,
val peers: List<PeerDto> = emptyList(),
val texts: List<TextDto> = emptyList(),
val media: List<MediaDto> = emptyList(),
val transfers: List<TransferDto> = emptyList(),
val scan: ScanDto = ScanDto(false, "", 0, 0, 0)
)
object ApiClient {
private const val BASE_HOST = "127.0.0.1:${RamshareService.UI_PORT}"
private const val BASE_URL = "http://$BASE_HOST"
private fun openConn(path: String, method: String, timeoutMs: Int = 5000): HttpURLConnection {
val conn = (URL("$BASE_URL$path").openConnection() as HttpURLConnection)
conn.requestMethod = method
conn.connectTimeout = timeoutMs
conn.readTimeout = timeoutMs
conn.setRequestProperty("Host", BASE_HOST)
conn.setRequestProperty("Origin", BASE_URL)
return conn
}
private fun readError(conn: HttpURLConnection): String {
val stream = conn.errorStream ?: return "Ошибка ${conn.responseCode}"
return BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
.ifBlank { "Ошибка ${conn.responseCode}" }
}
suspend fun fetchState(): NodeStateDto = withContext(Dispatchers.IO) {
val conn = openConn("/ui/state", "GET", 3000)
try {
if (conn.responseCode !in 200..299) throw Exception(readError(conn))
val text = BufferedReader(InputStreamReader(conn.inputStream, Charsets.UTF_8)).use { it.readText() }
val j = JSONObject(text)
val peersArr = j.optJSONArray("peers") ?: JSONArray()
val peers = (0 until peersArr.length()).map { i ->
val p = peersArr.getJSONObject(i)
val ipsArr = p.optJSONArray("ips") ?: JSONArray()
val ips = (0 until ipsArr.length()).map { k -> ipsArr.getString(k) }
PeerDto(
id = p.optString("id"),
name = p.optString("name", "Unknown"),
ips = ips,
port = p.optInt("port", 8443),
fingerprint = p.optString("fingerprint"),
pinCode = p.optString("pin_code"),
verified = p.optBoolean("verified"),
trustsMe = p.optBoolean("trusts_me"),
online = p.optBoolean("online"),
lastSeen = p.optLong("last_seen", 0L),
lastPing = p.optLong("last_ping", 0L),
failCount = p.optInt("fail_count", 0)
)
}
val textsArr = j.optJSONArray("texts") ?: JSONArray()
val texts = (0 until textsArr.length()).map { i ->
val t = textsArr.getJSONObject(i)
TextDto(
sender = t.optString("sender"),
text = t.optString("text"),
timestamp = t.optLong("timestamp")
)
}
val mediaArr = j.optJSONArray("media") ?: JSONArray()
val media = (0 until mediaArr.length()).map { i ->
val m = mediaArr.getJSONObject(i)
MediaDto(
sender = m.optString("sender"),
filename = m.optString("filename"),
savedPath = m.optString("saved_path"),
mime = m.optString("mime"),
size = m.optLong("size"),
timestamp = m.optLong("timestamp"),
previewable = m.optBoolean("previewable"),
risky = m.optBoolean("risky")
)
}
val trArr = j.optJSONArray("transfers") ?: JSONArray()
val transfers = (0 until trArr.length()).map { i ->
val tr = trArr.getJSONObject(i)
val err = if (tr.isNull("error")) null else tr.optString("error")
TransferDto(
id = tr.optString("id"),
filename = tr.optString("filename"),
peerName = tr.optString("peer_name"),
direction = tr.optString("direction"),
transferredBytes = tr.optLong("transferred_bytes"),
totalBytes = tr.optLong("total_bytes"),
percent = tr.optInt("percent"),
done = tr.optBoolean("done"),
error = err
)
}
val sc = j.optJSONObject("scan") ?: JSONObject()
val scan = ScanDto(
active = sc.optBoolean("active"),
range = sc.optString("range"),
scanned = sc.optInt("scanned"),
total = sc.optInt("total"),
found = sc.optInt("found")
)
NodeStateDto(
deviceName = j.optString("device_name"),
myIp = j.optString("my_ip"),
p2pPort = j.optInt("p2p_port", 8443),
myFingerprint = j.optString("my_fingerprint"),
myPin = j.optString("my_pin"),
mediaDir = j.optString("media_dir"),
pingIntervalSec = j.optInt("ping_interval_sec", 15),
lastPing = j.optLong("last_ping", 0L),
maxPingFails = j.optInt("max_ping_fails", 5),
now = j.optLong("now", System.currentTimeMillis() / 1000L),
peers = peers,
texts = texts,
media = media,
transfers = transfers,
scan = scan
)
} finally {
conn.disconnect()
}
}
suspend fun postJson(path: String, body: JSONObject, timeoutMs: Int = 15000): String =
withContext(Dispatchers.IO) {
val conn = openConn(path, "POST", timeoutMs)
try {
val bytes = body.toString().toByteArray(Charsets.UTF_8)
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
conn.setFixedLengthStreamingMode(bytes.size)
conn.outputStream.use { it.write(bytes) }
if (conn.responseCode !in 200..299) throw Exception(readError(conn))
BufferedReader(InputStreamReader(conn.inputStream, Charsets.UTF_8)).use { it.readText() }
} finally {
conn.disconnect()
}
}
suspend fun postEmpty(path: String, timeoutMs: Int = 15000) = withContext(Dispatchers.IO) {
val conn = openConn(path, "POST", timeoutMs)
try {
conn.doOutput = true
conn.setFixedLengthStreamingMode(0)
conn.outputStream.close()
if (conn.responseCode !in 200..299) throw Exception(readError(conn))
} finally {
conn.disconnect()
}
}
/** Потоковая отправка файла из ContentResolver напрямую в сокет с O(1) потреблением ОЗУ. */
suspend fun sendFileStream(resolver: ContentResolver, peerId: String, uri: Uri): String =
withContext(Dispatchers.IO) {
var displayName = "file.bin"
var fileSize = -1L
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null)
?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
if (nameIdx >= 0 && !cursor.isNull(nameIdx)) {
displayName = cursor.getString(nameIdx)
}
if (sizeIdx >= 0 && !cursor.isNull(sizeIdx)) {
fileSize = cursor.getLong(sizeIdx)
}
}
}
if (fileSize <= 0L) {
resolver.openFileDescriptor(uri, "r")?.use { pfd ->
fileSize = pfd.statSize
}
}
if (fileSize <= 0L) throw Exception("Не удалось определить размер файла (или файл пуст)")
val encodedName = URLEncoder.encode(displayName, "UTF-8").replace("+", "%20")
val conn = openConn("/ui/send-media", "POST", 0)
try {
conn.doOutput = true
conn.setRequestProperty("X-Target-Id", peerId)
conn.setRequestProperty("X-Filename", encodedName)
conn.setRequestProperty("Content-Type", "application/octet-stream")
conn.setFixedLengthStreamingMode(fileSize)
val input = resolver.openInputStream(uri)
?: throw Exception("Не удалось открыть выбранный файл")
input.use { ins ->
conn.outputStream.use { outs ->
val buf = ByteArray(64 * 1024)
while (true) {
val r = ins.read(buf)
if (r <= 0) break
outs.write(buf, 0, r)
}
outs.flush()
}
}
if (conn.responseCode !in 200..299) throw Exception(readError(conn))
displayName
} finally {
conn.disconnect()
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,16 @@
package com.ramshare.app
object RamshareNative {
init {
System.loadLibrary("ramshare_ui")
}
external fun startServer(
baseDir: String,
deviceName: String,
p2pPort: Int,
uiPort: Int
): Boolean
external fun stopServer()
}
@@ -0,0 +1,173 @@
package com.ramshare.app
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
class RamshareService : Service() {
companion object {
const val ACTION_START = "com.ramshare.app.START"
const val ACTION_STOP = "com.ramshare.app.STOP"
const val CHANNEL_ID = "ramshare_node_channel"
const val NOTIF_ID = 1001
const val P2P_PORT = 8443
const val UI_PORT = 8080
}
private var multicastLock: WifiManager.MulticastLock? = null
private var wifiLock: WifiManager.WifiLock? = null
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_STOP) {
stopNodeAndSelf()
return START_NOT_STICKY
}
val notification = buildNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIF_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIF_ID, notification)
}
acquireLocks()
val model = (Build.MANUFACTURER + "-" + Build.MODEL)
.replace(Regex("[^a-zA-Z0-9_\\- ]"), "")
.trim()
.ifEmpty { "Android-Node" }
Thread {
RamshareNative.startServer(
baseDir = filesDir.absolutePath,
deviceName = model,
p2pPort = P2P_PORT,
uiPort = UI_PORT
)
}.start()
return START_STICKY
}
private fun acquireLocks() {
val wifi = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
if (multicastLock == null) {
multicastLock = wifi?.createMulticastLock("ramshare:multicast")?.apply {
setReferenceCounted(false)
acquire()
}
}
if (wifiLock == null) {
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
WifiManager.WIFI_MODE_FULL_LOW_LATENCY
} else {
@Suppress("DEPRECATION")
WifiManager.WIFI_MODE_FULL_HIGH_PERF
}
wifiLock = wifi?.createWifiLock(mode, "ramshare:wifilock")?.apply {
setReferenceCounted(false)
acquire()
}
}
if (wakeLock == null) {
val pm = getSystemService(Context.POWER_SERVICE) as? PowerManager
wakeLock = pm?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ramshare:wakelock")?.apply {
setReferenceCounted(false)
acquire()
}
}
}
private fun releaseLocks() {
runCatching { if (multicastLock?.isHeld == true) multicastLock?.release() }
runCatching { if (wifiLock?.isHeld == true) wifiLock?.release() }
runCatching { if (wakeLock?.isHeld == true) wakeLock?.release() }
multicastLock = null
wifiLock = null
wakeLock = null
}
private fun stopNodeAndSelf() {
RamshareNative.stopServer()
releaseLocks()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
stopSelf()
}
override fun onDestroy() {
RamshareNative.stopServer()
releaseLocks()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val ch = NotificationChannel(
CHANNEL_ID,
"Узел LAN Share",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Фоновая работа P2P-узла и приём файлов в локальной сети"
}
val nm = getSystemService(NotificationManager::class.java)
nm?.createNotificationChannel(ch)
}
}
private fun buildNotification(): Notification {
val openIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val stopIntent = PendingIntent.getService(
this,
1,
Intent(this, RamshareService::class.java).apply { action = ACTION_STOP },
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("LAN Share активен")
.setContentText("P2P mTLS порт $P2P_PORT • Ожидание передач")
.setSmallIcon(android.R.drawable.stat_sys_upload_done)
.setContentIntent(openIntent)
.setOngoing(true)
.addAction(
Notification.Action.Builder(
null,
"Остановить",
stopIntent
).build()
)
.build()
}
}
Binary file not shown.
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">LAN Share</string>
</resources>
@@ -0,0 +1,6 @@
<resources>
<style name="Theme.RamShare" parent="android:Theme.Material.NoActionBar">
<item name="android:statusBarColor">#0F172A</item>
<item name="android:navigationBarColor">#0F172A</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="received_media" path="received_media/" />
</paths>