main funcions fixes
This commit is contained in:
62
android-client/app/app.iml
Normal file
62
android-client/app/app.iml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="7.2.2" />
|
||||
<option name="LAST_KNOWN_AGP_VERSION" value="7.2.2" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="debug" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
|
||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="kotlin-language" name="Kotlin">
|
||||
<configuration version="5" platform="JVM 1.8" allPlatforms="JVM [1.8]" useProjectSettings="false">
|
||||
<compilerSettings>
|
||||
<option name="additionalArguments" value="-java-parameters" />
|
||||
</compilerSettings>
|
||||
<compilerArguments>
|
||||
<stringArguments>
|
||||
<stringArg name="jvmTarget" arg="1.8" />
|
||||
</stringArguments>
|
||||
</compilerArguments>
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/classes" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 32 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="Gradle: androidx.core:core-ktx:1.8.0@aar" level="project" />
|
||||
<orderEntry type="library" exported="" name="Gradle: androidx.appcompat:appcompat:1.5.0@aar" level="project" />
|
||||
<orderEntry type="library" exported="" name="Gradle: com.google.android.material:material:1.6.1@aar" level="project" />
|
||||
<orderEntry type="library" exported="" name="Gradle: androidx.constraintlayout:constraintlayout:2.1.4@aar" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,10 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'com.godeye.android'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
@@ -25,12 +22,12 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '21'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -42,25 +39,28 @@ android {
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
|
||||
|
||||
// Socket.IO for WebSocket communication
|
||||
implementation 'io.socket:socket.io-client:2.1.0'
|
||||
|
||||
// WebRTC
|
||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||
|
||||
// Socket.IO
|
||||
implementation 'io.socket:socket.io-client:2.1.0'
|
||||
|
||||
// Camera
|
||||
implementation 'androidx.camera:camera-core:1.3.0'
|
||||
implementation 'androidx.camera:camera-camera2:1.3.0'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.3.0'
|
||||
implementation 'androidx.camera:camera-view:1.3.0'
|
||||
|
||||
// JSON
|
||||
|
||||
// Camera support
|
||||
implementation 'androidx.camera:camera-core:1.3.1'
|
||||
implementation 'androidx.camera:camera-camera2:1.3.1'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.3.1'
|
||||
implementation 'androidx.camera:camera-view:1.3.1'
|
||||
|
||||
// JSON parsing
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
|
||||
// Coroutines
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
||||
@@ -4,253 +4,134 @@ import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.godeye.android.camera.CameraManager
|
||||
import com.godeye.android.databinding.ActivityMainBinding
|
||||
import com.godeye.android.network.SocketManager
|
||||
import com.godeye.android.webrtc.WebRTCManager
|
||||
import org.webrtc.EglBase
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var socketManager: SocketManager
|
||||
private lateinit var cameraManager: CameraManager
|
||||
private lateinit var webRTCManager: WebRTCManager
|
||||
private lateinit var eglBase: EglBase
|
||||
|
||||
private val deviceId by lazy {
|
||||
Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
||||
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var tvDeviceId: TextView
|
||||
private lateinit var btnConnect: Button
|
||||
private lateinit var btnDisconnect: Button
|
||||
private lateinit var etServerUrl: EditText
|
||||
private lateinit var rvSessions: LinearLayout
|
||||
|
||||
private val deviceId: String by lazy {
|
||||
Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val PERMISSION_REQUEST_CODE = 100
|
||||
private const val PERMISSIONS_REQUEST_CODE = 100
|
||||
private val REQUIRED_PERMISSIONS = arrayOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.INTERNET,
|
||||
Manifest.permission.ACCESS_NETWORK_STATE
|
||||
Manifest.permission.INTERNET
|
||||
)
|
||||
|
||||
private const val SERVER_URL = "http://10.0.2.2:3000" // Для эмулятора, для реального устройства укажите IP сервера
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initViews()
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
|
||||
private fun initViews() {
|
||||
tvStatus = findViewById(R.id.tvStatus)
|
||||
tvDeviceId = findViewById(R.id.tvDeviceId)
|
||||
btnConnect = findViewById(R.id.btnConnect)
|
||||
btnDisconnect = findViewById(R.id.btnDisconnect)
|
||||
etServerUrl = findViewById(R.id.etServerUrl)
|
||||
rvSessions = findViewById(R.id.rvSessions)
|
||||
|
||||
tvDeviceId.text = "Device ID: $deviceId"
|
||||
etServerUrl.setText("http://192.168.1.100:3001")
|
||||
|
||||
btnConnect.setOnClickListener {
|
||||
connectToServer()
|
||||
}
|
||||
|
||||
btnDisconnect.setOnClickListener {
|
||||
disconnectFromServer()
|
||||
}
|
||||
|
||||
updateUI(false)
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
val missingPermissions = REQUIRED_PERMISSIONS.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
|
||||
if (missingPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
initializeComponents()
|
||||
ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), PERMISSIONS_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
val allPermissionsGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||
|
||||
if (allPermissionsGranted) {
|
||||
initializeComponents()
|
||||
} else {
|
||||
showPermissionDeniedDialog()
|
||||
|
||||
if (requestCode == PERMISSIONS_REQUEST_CODE) {
|
||||
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||
if (!allGranted) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Необходимы разрешения")
|
||||
.setMessage("Для работы приложения необходимы разрешения на камеру, микрофон и интернет")
|
||||
.setPositiveButton("OK") { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPermissionDeniedDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Необходимы разрешения")
|
||||
.setMessage("Для работы приложения необходим доступ к камере, микрофону и интернету")
|
||||
.setPositiveButton("OK") { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun initializeComponents() {
|
||||
try {
|
||||
// Инициализируем EGL контекст для WebRTC
|
||||
eglBase = EglBase.create()
|
||||
|
||||
// Инициализируем менеджеры
|
||||
cameraManager = CameraManager(this, eglBase)
|
||||
webRTCManager = WebRTCManager(this, cameraManager)
|
||||
socketManager = SocketManager(SERVER_URL)
|
||||
|
||||
setupSocketCallbacks()
|
||||
setupWebRTCCallbacks()
|
||||
setupUI()
|
||||
connectToServer()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error initializing components", e)
|
||||
Toast.makeText(this, "Ошибка инициализации: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSocketCallbacks() {
|
||||
socketManager.onCameraRequest = { sessionId, operatorId, cameraTypeStr ->
|
||||
runOnUiThread {
|
||||
val cameraType = parseCameraType(cameraTypeStr)
|
||||
showCameraRequestDialog(sessionId, operatorId, cameraType)
|
||||
}
|
||||
}
|
||||
|
||||
socketManager.onCameraSwitch = { sessionId, cameraTypeStr ->
|
||||
runOnUiThread {
|
||||
val cameraType = parseCameraType(cameraTypeStr)
|
||||
val success = webRTCManager.switchCamera(sessionId, cameraType)
|
||||
updateCameraStatus(sessionId, if (success) "Камера переключена на: $cameraTypeStr" else "Ошибка переключения камеры")
|
||||
}
|
||||
}
|
||||
|
||||
socketManager.onCameraDisconnect = { sessionId ->
|
||||
runOnUiThread {
|
||||
webRTCManager.closeSession(sessionId)
|
||||
updateConnectionStatus("Оператор отключился")
|
||||
}
|
||||
}
|
||||
|
||||
socketManager.onWebRTCAnswer = { sessionId, answer ->
|
||||
webRTCManager.handleAnswer(sessionId, answer)
|
||||
}
|
||||
|
||||
socketManager.onWebRTCIceCandidate = { sessionId, candidate ->
|
||||
webRTCManager.handleICECandidate(sessionId, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWebRTCCallbacks() {
|
||||
webRTCManager.onOfferCreated = { sessionId, offer ->
|
||||
socketManager.sendWebRTCOffer(sessionId, offer)
|
||||
}
|
||||
|
||||
webRTCManager.onAnswerCreated = { sessionId, answer ->
|
||||
socketManager.sendWebRTCAnswer(sessionId, answer)
|
||||
}
|
||||
|
||||
webRTCManager.onICECandidate = { sessionId, candidate ->
|
||||
socketManager.sendICECandidate(sessionId, candidate)
|
||||
}
|
||||
|
||||
webRTCManager.onConnectionStateChanged = { sessionId, state ->
|
||||
runOnUiThread {
|
||||
val statusText = when (state) {
|
||||
org.webrtc.PeerConnection.PeerConnectionState.CONNECTING -> "Подключение..."
|
||||
org.webrtc.PeerConnection.PeerConnectionState.CONNECTED -> "Подключено"
|
||||
org.webrtc.PeerConnection.PeerConnectionState.DISCONNECTED -> "Отключено"
|
||||
org.webrtc.PeerConnection.PeerConnectionState.FAILED -> "Ошибка подключения"
|
||||
org.webrtc.PeerConnection.PeerConnectionState.CLOSED -> "Соединение закрыто"
|
||||
else -> "Неизвестно"
|
||||
}
|
||||
updateConnectionStatus("$statusText (Сессия: $sessionId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
// Показываем доступные типы камер
|
||||
val availableTypes = cameraManager.getAvailableCameraTypes()
|
||||
binding.availableCamerasText.text = "Доступные камеры: ${availableTypes.joinToString(", ")}"
|
||||
|
||||
// Показываем Device ID
|
||||
binding.deviceIdText.text = "Device ID: $deviceId"
|
||||
|
||||
// Статус подключения
|
||||
updateConnectionStatus("Инициализация...")
|
||||
}
|
||||
|
||||
|
||||
private fun connectToServer() {
|
||||
val deviceInfo = mapOf(
|
||||
"model" to android.os.Build.MODEL,
|
||||
"manufacturer" to android.os.Build.MANUFACTURER,
|
||||
"androidVersion" to android.os.Build.VERSION.RELEASE,
|
||||
"appVersion" to BuildConfig.VERSION_NAME
|
||||
)
|
||||
|
||||
socketManager.connect(deviceId, deviceInfo)
|
||||
updateConnectionStatus("Подключение к серверу...")
|
||||
}
|
||||
|
||||
private fun handleCameraRequest(sessionId: String, operatorId: String, cameraType: String) {
|
||||
runOnUiThread {
|
||||
binding.tvStatus.text = "Запрос доступа к камере от оператора: $operatorId"
|
||||
binding.tvCameraType.text = "Тип камеры: $cameraType"
|
||||
|
||||
val serverUrl = etServerUrl.text.toString().trim()
|
||||
if (serverUrl.isEmpty()) {
|
||||
Toast.makeText(this, "Введите URL сервера", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
tvStatus.text = "Подключение..."
|
||||
btnConnect.isEnabled = false
|
||||
|
||||
// TODO: Implement actual connection logic
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
cameraManager.startCamera(cameraType) { success ->
|
||||
if (success) {
|
||||
val streamUrl = webRTCManager.createOffer(sessionId) { offer ->
|
||||
socketManager.sendWebRTCOffer(sessionId, offer)
|
||||
}
|
||||
socketManager.respondToCameraRequest(sessionId, true, streamUrl)
|
||||
updateConnectionStatus("Трансляция активна")
|
||||
} else {
|
||||
socketManager.respondToCameraRequest(sessionId, false, null, "Не удалось запустить камеру")
|
||||
updateConnectionStatus("Ошибка запуска камеры")
|
||||
}
|
||||
}
|
||||
// Simulate connection
|
||||
kotlinx.coroutines.delay(2000)
|
||||
updateUI(true)
|
||||
tvStatus.text = "Подключено к $serverUrl"
|
||||
Toast.makeText(this@MainActivity, "Подключено!", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error starting camera", e)
|
||||
socketManager.respondToCameraRequest(sessionId, false, null, e.message)
|
||||
updateUI(false)
|
||||
tvStatus.text = "Ошибка подключения: ${e.message}"
|
||||
Toast.makeText(this@MainActivity, "Ошибка: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCameraSwitch(sessionId: String, cameraType: String) {
|
||||
runOnUiThread {
|
||||
binding.tvCameraType.text = "Переключение на: $cameraType"
|
||||
cameraManager.switchCamera(cameraType) { success ->
|
||||
if (success) {
|
||||
updateConnectionStatus("Камера переключена на $cameraType")
|
||||
} else {
|
||||
updateConnectionStatus("Ошибка переключения камеры")
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectFromServer() {
|
||||
tvStatus.text = "Отключение..."
|
||||
btnDisconnect.isEnabled = false
|
||||
|
||||
// TODO: Implement actual disconnection logic
|
||||
lifecycleScope.launch {
|
||||
kotlinx.coroutines.delay(1000)
|
||||
updateUI(false)
|
||||
tvStatus.text = "Отключено"
|
||||
Toast.makeText(this@MainActivity, "Отключено", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCameraDisconnect(sessionId: String) {
|
||||
runOnUiThread {
|
||||
cameraManager.stopCamera()
|
||||
webRTCManager.endSession(sessionId)
|
||||
updateConnectionStatus("Трансляция завершена")
|
||||
binding.tvCameraType.text = ""
|
||||
}
|
||||
|
||||
private fun updateUI(connected: Boolean) {
|
||||
btnConnect.isEnabled = !connected
|
||||
btnDisconnect.isEnabled = connected
|
||||
etServerUrl.isEnabled = !connected
|
||||
}
|
||||
|
||||
private fun updateConnectionStatus(status: String) {
|
||||
runOnUiThread {
|
||||
binding.tvStatus.text = status
|
||||
Log.i("MainActivity", "Status: $status")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
binding.btnDisconnect.setOnClickListener {
|
||||
socketManager.disconnect()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraManager.release()
|
||||
webRTCManager.release()
|
||||
socketManager.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.godeye.android.camera
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraManager as SystemCameraManager
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.util.Log
|
||||
|
||||
class CameraManager(private val context: Context) {
|
||||
|
||||
private val systemCameraManager = context.getSystemService(Context.CAMERA_SERVICE) as SystemCameraManager
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraManager"
|
||||
}
|
||||
|
||||
fun getAvailableCameras(): List<String> {
|
||||
val availableCameras = mutableListOf<String>()
|
||||
|
||||
try {
|
||||
val cameraIds = systemCameraManager.cameraIdList
|
||||
|
||||
for (cameraId in cameraIds) {
|
||||
val characteristics = systemCameraManager.getCameraCharacteristics(cameraId)
|
||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||
|
||||
when (facing) {
|
||||
CameraCharacteristics.LENS_FACING_BACK -> {
|
||||
availableCameras.add("back")
|
||||
|
||||
// Проверяем на наличие широкоугольной или телекамеры
|
||||
val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
||||
if (focalLengths != null && focalLengths.size > 1) {
|
||||
// Предполагаем, что есть дополнительные камеры
|
||||
if (focalLengths.minOrNull() ?: 0f < 3.0f) {
|
||||
availableCameras.add("wide")
|
||||
}
|
||||
if (focalLengths.maxOrNull() ?: 0f > 6.0f) {
|
||||
availableCameras.add("telephoto")
|
||||
}
|
||||
}
|
||||
}
|
||||
CameraCharacteristics.LENS_FACING_FRONT -> {
|
||||
availableCameras.add("front")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем дубликаты
|
||||
return availableCameras.distinct()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting available cameras", e)
|
||||
// Возвращаем минимальный набор по умолчанию
|
||||
return listOf("back", "front")
|
||||
}
|
||||
}
|
||||
|
||||
fun getCameraId(cameraType: String): String? {
|
||||
try {
|
||||
val cameraIds = systemCameraManager.cameraIdList
|
||||
|
||||
for (cameraId in cameraIds) {
|
||||
val characteristics = systemCameraManager.getCameraCharacteristics(cameraId)
|
||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||
|
||||
when (cameraType) {
|
||||
"back" -> {
|
||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
||||
return cameraId
|
||||
}
|
||||
}
|
||||
"front" -> {
|
||||
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
|
||||
return cameraId
|
||||
}
|
||||
}
|
||||
"wide", "telephoto" -> {
|
||||
// Для простоты используем основную заднюю камеру
|
||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
||||
return cameraId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting camera ID for type: $cameraType", e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getSupportedSizes(cameraId: String): List<android.util.Size> {
|
||||
return try {
|
||||
val characteristics = systemCameraManager.getCameraCharacteristics(cameraId)
|
||||
val configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
||||
configs?.getOutputSizes(android.graphics.SurfaceTexture::class.java)?.toList() ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting supported sizes for camera: $cameraId", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.godeye.android.network
|
||||
|
||||
import io.socket.client.IO
|
||||
import io.socket.client.Socket
|
||||
import io.socket.emitter.Emitter
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import android.util.Log
|
||||
|
||||
class SocketManager(
|
||||
private val deviceId: String,
|
||||
private val onCameraRequest: (String, String, String) -> Unit,
|
||||
private val onConnectionStatus: (String) -> Unit,
|
||||
private val onError: (String) -> Unit
|
||||
) {
|
||||
private var socket: Socket? = null
|
||||
private val serverUrl = "http://10.0.2.2:3001" // Android emulator localhost
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SocketManager"
|
||||
}
|
||||
|
||||
fun connect(availableCameras: List<String>) {
|
||||
try {
|
||||
val opts = IO.Options()
|
||||
opts.reconnection = true
|
||||
opts.timeout = 10000
|
||||
|
||||
socket = IO.socket(serverUrl, opts)
|
||||
|
||||
socket?.let { sock ->
|
||||
sock.on(Socket.EVENT_CONNECT, onConnect)
|
||||
sock.on(Socket.EVENT_DISCONNECT, onDisconnect)
|
||||
sock.on(Socket.EVENT_CONNECT_ERROR, onConnectError)
|
||||
sock.on("register:success", onRegisterSuccess)
|
||||
sock.on("register:error", onRegisterError)
|
||||
sock.on("camera:request", onCameraRequestReceived)
|
||||
sock.on("camera:switch", onCameraSwitchReceived)
|
||||
sock.on("camera:disconnect", onCameraDisconnectReceived)
|
||||
sock.on("webrtc:offer", onWebRTCOffer)
|
||||
sock.on("webrtc:answer", onWebRTCAnswer)
|
||||
sock.on("webrtc:ice-candidate", onWebRTCIceCandidate)
|
||||
|
||||
sock.connect()
|
||||
|
||||
// Регистрация устройства
|
||||
val deviceInfo = JSONObject().apply {
|
||||
put("model", android.os.Build.MODEL)
|
||||
put("manufacturer", android.os.Build.MANUFACTURER)
|
||||
put("androidVersion", android.os.Build.VERSION.RELEASE)
|
||||
put("appVersion", "1.0.0")
|
||||
put("availableCameras", JSONArray(availableCameras))
|
||||
}
|
||||
|
||||
val registerData = JSONObject().apply {
|
||||
put("deviceId", deviceId)
|
||||
put("deviceInfo", deviceInfo)
|
||||
}
|
||||
|
||||
sock.emit("register:android", registerData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Connection error", e)
|
||||
onError("Ошибка подключения: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
socket?.disconnect()
|
||||
socket?.off()
|
||||
socket = null
|
||||
onConnectionStatus("Отключено")
|
||||
}
|
||||
|
||||
fun acceptCameraRequest(sessionId: String, accepted: Boolean) {
|
||||
val response = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
put("accepted", accepted)
|
||||
put("message", if (accepted) "Доступ разрешен" else "Доступ отклонен")
|
||||
}
|
||||
socket?.emit("camera:response", response)
|
||||
Log.d(TAG, "Camera request response sent: $accepted")
|
||||
}
|
||||
|
||||
// Socket event handlers
|
||||
private val onConnect = Emitter.Listener {
|
||||
Log.d(TAG, "Connected to server")
|
||||
onConnectionStatus("Подключено")
|
||||
}
|
||||
|
||||
private val onDisconnect = Emitter.Listener {
|
||||
Log.d(TAG, "Disconnected from server")
|
||||
onConnectionStatus("Отключено")
|
||||
}
|
||||
|
||||
private val onConnectError = Emitter.Listener { args ->
|
||||
Log.e(TAG, "Connection error: ${args[0]}")
|
||||
onError("Ошибка подключения к серверу")
|
||||
}
|
||||
|
||||
private val onRegisterSuccess = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
Log.d(TAG, "Registration successful: ${data.getString("deviceId")}")
|
||||
onConnectionStatus("Зарегистрировано")
|
||||
}
|
||||
|
||||
private val onRegisterError = Emitter.Listener { args ->
|
||||
val error = args[0] as JSONObject
|
||||
Log.e(TAG, "Registration error: ${error.getString("message")}")
|
||||
onError("Ошибка регистрации: ${error.getString("message")}")
|
||||
}
|
||||
|
||||
private val onCameraRequestReceived = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val operatorId = data.getString("operatorId")
|
||||
val cameraType = data.getString("cameraType")
|
||||
|
||||
Log.d(TAG, "Camera request received: $sessionId, $operatorId, $cameraType")
|
||||
onCameraRequest(sessionId, operatorId, cameraType)
|
||||
}
|
||||
|
||||
private val onCameraSwitchReceived = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val cameraType = data.getString("cameraType")
|
||||
|
||||
Log.d(TAG, "Camera switch request: $sessionId -> $cameraType")
|
||||
// TODO: Implement camera switching
|
||||
}
|
||||
|
||||
private val onCameraDisconnectReceived = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
|
||||
Log.d(TAG, "Camera disconnect request: $sessionId")
|
||||
// TODO: Implement camera disconnect
|
||||
}
|
||||
|
||||
private val onWebRTCOffer = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val offer = data.getJSONObject("offer")
|
||||
|
||||
Log.d(TAG, "WebRTC offer received for session: $sessionId")
|
||||
// TODO: Handle WebRTC offer
|
||||
}
|
||||
|
||||
private val onWebRTCAnswer = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val answer = data.getJSONObject("answer")
|
||||
|
||||
Log.d(TAG, "WebRTC answer received for session: $sessionId")
|
||||
// TODO: Handle WebRTC answer
|
||||
}
|
||||
|
||||
private val onWebRTCIceCandidate = Emitter.Listener { args ->
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val candidate = data.getJSONObject("candidate")
|
||||
|
||||
Log.d(TAG, "WebRTC ICE candidate received for session: $sessionId")
|
||||
// TODO: Handle WebRTC ICE candidate
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res/android-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<!-- Status Section -->
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Инициализация..."
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:background="@color/status_background"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Device Info -->
|
||||
<TextView
|
||||
android:id="@+id/deviceIdText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Device ID: ---"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/availableCamerasText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Доступные камеры: ---"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Camera Type -->
|
||||
<TextView
|
||||
android:id="@+id/tvCameraType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Video Preview -->
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/localVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@color/video_background" />
|
||||
|
||||
<!-- Connection Status -->
|
||||
<TextView
|
||||
android:id="@+id/connectionStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Статус подключения: Отключено"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Control Buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnConnect"
|
||||
android:layout_width="0dp"
|
||||
<!-- Header -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Подключиться"
|
||||
android:layout_marginEnd="8dp" />
|
||||
android:text="GodEye Signal Center"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="@color/design_default_color_primary" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDisconnect"
|
||||
android:layout_width="0dp"
|
||||
<!-- Device Info -->
|
||||
<TextView
|
||||
android:id="@+id/tvDeviceId"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Отключиться"
|
||||
android:layout_marginStart="8dp"
|
||||
android:enabled="false" />
|
||||
android:text="Device ID: ..."
|
||||
android:textSize="14sp"
|
||||
android:background="@color/design_default_color_surface"
|
||||
android:padding="12dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Connection Status -->
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Отключено"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textAlignment="center"
|
||||
android:padding="12dp"
|
||||
android:background="@color/design_default_color_surface"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<!-- Server Configuration -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройки сервера"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etServerUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="URL сервера (например: http://192.168.1.100:3001)"
|
||||
android:inputType="textUri"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Control Buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnConnect"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Подключиться"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDisconnect"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Отключиться"
|
||||
android:layout_marginStart="8dp"
|
||||
android:enabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Active Sessions -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Активные сессии"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rvSessions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/design_default_color_surface"
|
||||
android:padding="16dp"
|
||||
android:minHeight="100dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Нет активных сессий"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/darker_gray" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 164 B After Width: | Height: | Size: 0 B |
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.GodEye" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<style name="Theme.GodEye" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/design_default_color_primary</item>
|
||||
<item name="colorPrimaryVariant">@color/design_default_color_primary_variant</item>
|
||||
<item name="colorOnPrimary">@color/design_default_color_on_primary</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="colorSecondary">@color/design_default_color_secondary</item>
|
||||
<item name="colorSecondaryVariant">@color/design_default_color_secondary_variant</item>
|
||||
<item name="colorOnSecondary">@color/design_default_color_on_secondary</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="device_prefs"/>
|
||||
<exclude domain="database" path="room_master_table.db"/>
|
||||
<!-- Exclude sensitive data from backup -->
|
||||
<exclude domain="sharedpref" path="secure_prefs.xml"/>
|
||||
</full-backup-content>
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<paths>
|
||||
<external-files-path name="external_files" path="."/>
|
||||
</paths>
|
||||
</resources>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user