diff --git a/android-client/.gradle/9.0-milestone-1/checksums/checksums.lock b/android-client/.gradle/9.0-milestone-1/checksums/checksums.lock new file mode 100644 index 0000000..46f8893 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/checksums/checksums.lock differ diff --git a/android-client/.gradle/9.0-milestone-1/checksums/md5-checksums.bin b/android-client/.gradle/9.0-milestone-1/checksums/md5-checksums.bin new file mode 100644 index 0000000..9d2824e Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/checksums/md5-checksums.bin differ diff --git a/android-client/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin b/android-client/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin new file mode 100644 index 0000000..0e43c17 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin differ diff --git a/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin b/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin new file mode 100644 index 0000000..1f49774 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin differ diff --git a/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock b/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock new file mode 100644 index 0000000..8735fbd Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock differ diff --git a/android-client/.gradle/9.0-milestone-1/fileChanges/last-build.bin b/android-client/.gradle/9.0-milestone-1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/fileChanges/last-build.bin differ diff --git a/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin b/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin new file mode 100644 index 0000000..8efabc2 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin differ diff --git a/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock b/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..64361a8 Binary files /dev/null and b/android-client/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock differ diff --git a/android-client/.gradle/9.0-milestone-1/gc.properties b/android-client/.gradle/9.0-milestone-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..d839cc4 Binary files /dev/null and b/android-client/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/android-client/.gradle/buildOutputCleanup/cache.properties b/android-client/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..91d7695 --- /dev/null +++ b/android-client/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Sun Sep 28 22:04:42 KST 2025 +gradle.version=9.0-milestone-1 diff --git a/android-client/.gradle/buildOutputCleanup/outputFiles.bin b/android-client/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..02e36e6 Binary files /dev/null and b/android-client/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/android-client/.gradle/config.properties b/android-client/.gradle/config.properties new file mode 100644 index 0000000..4d961ef --- /dev/null +++ b/android-client/.gradle/config.properties @@ -0,0 +1,2 @@ +#Sun Sep 28 22:05:42 KST 2025 +java.home=/snap/android-studio/205/jbr diff --git a/android-client/.gradle/vcs-1/gc.properties b/android-client/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-client/.run/app.run.xml b/android-client/.run/app.run.xml new file mode 100644 index 0000000..8e7623a --- /dev/null +++ b/android-client/.run/app.run.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android-client/android-client.iml b/android-client/android-client.iml deleted file mode 100644 index f76df2b..0000000 --- a/android-client/android-client.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android-client/app/build.gradle b/android-client/app/build.gradle new file mode 100644 index 0000000..fd50cec --- /dev/null +++ b/android-client/app/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.godeye.android' + compileSdk 34 + + defaultConfig { + applicationId "com.godeye.android" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + viewBinding true + buildConfig true + } +} + +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 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.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 + implementation 'com.google.code.gson:gson:2.10.1' + + // Testing + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/android-client/app/proguard-rules.pro b/android-client/app/proguard-rules.pro new file mode 100644 index 0000000..93183c1 --- /dev/null +++ b/android-client/app/proguard-rules.pro @@ -0,0 +1,29 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# WebRTC ProGuard rules +-keep class org.webrtc.** { *; } +-dontwarn org.webrtc.** + +# Socket.IO ProGuard rules +-keep class io.socket.** { *; } +-dontwarn io.socket.** diff --git a/android-client/app/src/main/AndroidManifest.xml b/android-client/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a016f17 --- /dev/null +++ b/android-client/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-client/app/src/main/java/com/godeye/android/MainActivity.kt b/android-client/app/src/main/java/com/godeye/android/MainActivity.kt new file mode 100644 index 0000000..506b422 --- /dev/null +++ b/android-client/app/src/main/java/com/godeye/android/MainActivity.kt @@ -0,0 +1,256 @@ +package com.godeye.android + +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 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 + +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) + } + + companion object { + private const val PERMISSION_REQUEST_CODE = 100 + private val REQUIRED_PERMISSIONS = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + ) + + 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) + + checkPermissions() + } + + 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() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, 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() + } + } + } + + 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" + + 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("Ошибка запуска камеры") + } + } + } catch (e: Exception) { + Log.e("MainActivity", "Error starting camera", e) + socketManager.respondToCameraRequest(sessionId, false, null, e.message) + } + } + } + + private fun handleCameraSwitch(sessionId: String, cameraType: String) { + runOnUiThread { + binding.tvCameraType.text = "Переключение на: $cameraType" + cameraManager.switchCamera(cameraType) { success -> + if (success) { + updateConnectionStatus("Камера переключена на $cameraType") + } else { + updateConnectionStatus("Ошибка переключения камеры") + } + } + } + } + + private fun handleCameraDisconnect(sessionId: String) { + runOnUiThread { + cameraManager.stopCamera() + webRTCManager.endSession(sessionId) + updateConnectionStatus("Трансляция завершена") + binding.tvCameraType.text = "" + } + } + + 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() + } +} \ No newline at end of file diff --git a/android-client/app/src/main/res/drawable/ic_launcher_foreground.xml b/android-client/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..3626962 --- /dev/null +++ b/android-client/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android-client/app/src/main/res/layout/activity_main.xml b/android-client/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ca64a44 --- /dev/null +++ b/android-client/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + +