android app

This commit is contained in:
2025-09-28 22:11:39 +09:00
parent 25cb9d9c8f
commit 40e016e128
41 changed files with 1625 additions and 11 deletions

View File

@@ -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'
}

29
android-client/app/proguard-rules.pro vendored Normal file
View File

@@ -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.**

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<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.MODIFY_AUDIO_SETTINGS" />
<!-- Hardware features -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GodEye"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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<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()
}
}
}
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()
}
}

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#ffffff"
android:pathData="M54,30c-13.3,0 -24,10.7 -24,24s10.7,24 24,24s24,-10.7 24,-24S67.3,30 54,30zM54,70c-8.8,0 -16,-7.2 -16,-16s7.2,-16 16,-16s16,7.2 16,16S62.8,70 54,70z"/>
<path android:fillColor="#ffffff"
android:pathData="M54,38c-8.8,0 -16,7.2 -16,16s7.2,16 16,16s16,-7.2 16,-16S62.8,38 54,38zM54,62c-4.4,0 -8,-3.6 -8,-8s3.6,-8 8,-8s8,3.6 8,8S58.4,62 54,62z"/>
</vector>

View File

@@ -0,0 +1,92 @@
<?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"
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">
<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>
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Custom colors -->
<color name="status_background">#E3F2FD</color>
<color name="video_background">#212121</color>
<color name="error_color">#F44336</color>
<color name="success_color">#4CAF50</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<color name="ic_launcher_background">#3DDC84</color>
</resources>

View File

@@ -0,0 +1,14 @@
<resources>
<string name="app_name">GodEye Signal Center</string>
<string name="permission_denied_title">Необходимы разрешения</string>
<string name="permission_denied_message">Для работы приложения необходим доступ к камере, микрофону и интернету</string>
<string name="camera_request_title">Запрос доступа к камере</string>
<string name="camera_request_message">Оператор %1$s запрашивает доступ к камере %2$s. Разрешить?</string>
<string name="allow">Разрешить</string>
<string name="deny">Отклонить</string>
<string name="connecting">Подключение...</string>
<string name="connected">Подключено</string>
<string name="disconnected">Отключено</string>
<string name="error">Ошибка</string>
</resources>

View File

@@ -0,0 +1,18 @@
<?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>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +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"/>
</full-backup-content>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-files-path name="external_files" path="."/>
</paths>
</resources>