Files
chat/docs/MOBILE_APP_INTEGRATION_GUIDE.md
Andrey K. Choi 3050e084fa
All checks were successful
continuous-integration/drone/push Build is passing
main functions commit
2025-10-19 19:50:00 +09:00

1184 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🚨 ПОЛНОЕ ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Доработка модуля экстренных сообщений для мобильного приложения
## 📋 ОБЗОР ПРОЕКТА
**Цель:** Интегрировать мобильное приложение с Emergency Service через WebSocket и REST API
**Архитектура:** Микросервисная система с JWT аутентификацией
**Основные компоненты:** Emergency Service (порт 8002), API Gateway (порт 8000), WebSocket подключения
---
## 🔧 ЭТАП 1: НАСТРОЙКА АУТЕНТИФИКАЦИИ
### 1.1 Удаление временных токенов
**Проблема:** В коде используются временные токены вида `temp_token_for_${email}`
**Задачи:**
```kotlin
// ❌ УДАЛИТЬ ЭТО:
val tempToken = "temp_token_for_${userEmail}"
headers["Authorization"] = "Bearer $tempToken"
// ✅ ЗАМЕНИТЬ НА:
val jwtToken = authManager.getValidJwtToken()
headers["Authorization"] = "Bearer $jwtToken"
```
### 1.2 Реализация JWT аутентификации
**Создать класс AuthManager:**
```kotlin
class AuthManager {
private val baseUrl = "http://YOUR_SERVER:8000"
private var jwtToken: String? = null
private var refreshToken: String? = null
suspend fun login(email: String, password: String): Result<LoginResponse> {
val loginData = LoginRequest(email, password)
return try {
val response = apiService.login(loginData)
if (response.isSuccessful) {
val authData = response.body()!!
jwtToken = authData.access_token
saveTokens(authData.access_token, authData.refresh_token)
Result.success(authData)
} else {
Result.failure(Exception("Login failed: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
fun getValidJwtToken(): String? {
return if (isTokenValid(jwtToken)) jwtToken else null
}
private fun isTokenValid(token: String?): Boolean {
// Проверка срока действия JWT токена
if (token.isNullOrEmpty()) return false
return try {
val payload = decodeJwtPayload(token)
val exp = payload["exp"] as? Long ?: 0
exp > System.currentTimeMillis() / 1000
} catch (e: Exception) {
false
}
}
}
```
### 1.3 Модели данных для аутентификации
```kotlin
data class LoginRequest(
val email: String,
val password: String
)
data class LoginResponse(
val access_token: String,
val token_type: String,
val user: UserData
)
data class UserData(
val id: Int,
val email: String,
val first_name: String?,
val last_name: String?
)
```
---
## 🌐 ЭТАП 2: НАСТРОЙКА REST API CLIENT
### 2.1 Создание Emergency API Service
```kotlin
interface EmergencyApiService {
@POST("api/v1/alert")
suspend fun createAlert(
@Body alertRequest: CreateAlertRequest,
@Header("Authorization") auth: String
): Response<EmergencyAlertResponse>
@GET("api/v1/alerts/my")
suspend fun getMyAlerts(
@Header("Authorization") auth: String
): Response<List<EmergencyAlertResponse>>
@GET("api/v1/alerts/active")
suspend fun getActiveAlerts(
@Header("Authorization") auth: String
): Response<List<EmergencyAlertResponse>>
@GET("api/v1/alerts/nearby")
suspend fun getNearbyAlerts(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double,
@Query("radius") radius: Int,
@Header("Authorization") auth: String
): Response<List<NearbyAlertResponse>>
@POST("api/v1/alert/{alertId}/respond")
suspend fun respondToAlert(
@Path("alertId") alertId: Int,
@Body response: AlertResponseRequest,
@Header("Authorization") auth: String
): Response<EmergencyResponseResponse>
@POST("api/v1/report")
suspend fun createReport(
@Body reportRequest: CreateReportRequest,
@Header("Authorization") auth: String
): Response<EmergencyReportResponse>
@POST("api/v1/safety-check")
suspend fun createSafetyCheck(
@Body safetyCheck: SafetyCheckRequest,
@Header("Authorization") auth: String
): Response<SafetyCheckResponse>
@GET("api/v1/stats")
suspend fun getStatistics(
@Header("Authorization") auth: String
): Response<EmergencyStatistics>
}
```
### 2.2 Модели данных для Emergency API
```kotlin
// Запросы
data class CreateAlertRequest(
val alert_type: String, // "medical", "fire", "police", "other"
val latitude: Double,
val longitude: Double,
val address: String? = null,
val description: String
)
data class CreateReportRequest(
val incident_type: String,
val latitude: Double,
val longitude: Double,
val address: String? = null,
val description: String,
val severity: String // "low", "medium", "high"
)
data class SafetyCheckRequest(
val latitude: Double,
val longitude: Double,
val status: String, // "safe", "unsafe", "need_help"
val message: String? = null
)
data class AlertResponseRequest(
val response_type: String, // "help_on_way", "safe_now", "false_alarm"
val message: String? = null
)
// Ответы
data class EmergencyAlertResponse(
val id: Int,
val alert_type: String,
val latitude: Double,
val longitude: Double,
val address: String?,
val description: String,
val status: String,
val created_at: String,
val responded_users_count: Int
)
data class NearbyAlertResponse(
val id: Int,
val alert_type: String,
val latitude: Double,
val longitude: Double,
val address: String?,
val distance: Double,
val created_at: String,
val responded_users_count: Int
)
data class EmergencyResponseResponse(
val id: Int,
val response_type: String,
val message: String?,
val created_at: String
)
data class EmergencyReportResponse(
val id: Int,
val incident_type: String,
val latitude: Double,
val longitude: Double,
val address: String?,
val description: String,
val severity: String,
val status: String,
val created_at: String
)
data class SafetyCheckResponse(
val id: Int,
val latitude: Double,
val longitude: Double,
val status: String,
val message: String?,
val created_at: String
)
data class EmergencyStatistics(
val total_alerts: Int,
val active_alerts: Int,
val resolved_alerts: Int,
val total_reports: Int,
val my_alerts_count: Int,
val my_responses_count: Int
)
```
---
## 📡 ЭТАП 3: РЕАЛИЗАЦИЯ WEBSOCKET ПОДКЛЮЧЕНИЯ
### 3.1 WebSocket Manager
```kotlin
class EmergencyWebSocketManager(
private val authManager: AuthManager,
private val coroutineScope: CoroutineScope
) {
private var webSocket: WebSocket? = null
private var isConnected = false
private val listeners = mutableListOf<EmergencyWebSocketListener>()
fun connect() {
val jwtToken = authManager.getValidJwtToken()
if (jwtToken == null) {
notifyError("No valid JWT token available")
return
}
val request = Request.Builder()
.url("ws://YOUR_SERVER:8002/api/v1/emergency/ws/current_user_id?token=$jwtToken")
.build()
val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
isConnected = true
notifyConnectionOpened()
// Отправляем ping для поддержания соединения
startPingKeepAlive()
}
override fun onMessage(webSocket: WebSocket, text: String) {
handleMessage(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
notifyConnectionClosing(code, reason)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isConnected = false
notifyError("WebSocket connection failed: ${t.message}")
// Автоматическое переподключение
scheduleReconnect()
}
})
}
private fun handleMessage(jsonMessage: String) {
try {
val message = Json.decodeFromString<WebSocketMessage>(jsonMessage)
when (message.type) {
"connection_established" -> {
notifyConnectionEstablished(message.user_id)
}
"emergency_alert" -> {
val alertData = Json.decodeFromString<EmergencyAlertResponse>(
Json.encodeToString(message.data)
)
notifyNewAlert(alertData)
}
"alert_update" -> {
val updateData = Json.decodeFromString<AlertUpdateData>(
Json.encodeToString(message.data)
)
notifyAlertUpdate(updateData)
}
"alert_response" -> {
val responseData = Json.decodeFromString<EmergencyResponseResponse>(
Json.encodeToString(message.data)
)
notifyAlertResponse(responseData)
}
}
} catch (e: Exception) {
notifyError("Failed to parse WebSocket message: ${e.message}")
}
}
fun sendMessage(message: Any) {
if (isConnected) {
val jsonMessage = Json.encodeToString(message)
webSocket?.send(jsonMessage)
}
}
fun addListener(listener: EmergencyWebSocketListener) {
listeners.add(listener)
}
fun removeListener(listener: EmergencyWebSocketListener) {
listeners.remove(listener)
}
private fun startPingKeepAlive() {
coroutineScope.launch {
while (isConnected) {
delay(30_000) // Ping каждые 30 секунд
sendMessage(mapOf("type" to "ping"))
}
}
}
private fun scheduleReconnect() {
coroutineScope.launch {
delay(5_000) // Ждем 5 секунд перед переподключением
connect()
}
}
}
interface EmergencyWebSocketListener {
fun onConnectionOpened()
fun onConnectionEstablished(userId: Int)
fun onNewAlert(alert: EmergencyAlertResponse)
fun onAlertUpdate(update: AlertUpdateData)
fun onAlertResponse(response: EmergencyResponseResponse)
fun onConnectionClosing(code: Int, reason: String)
fun onError(error: String)
}
@Serializable
data class WebSocketMessage(
val type: String,
val data: JsonElement? = null,
val user_id: Int? = null,
val message: String? = null
)
@Serializable
data class AlertUpdateData(
val alert_id: Int,
val status: String,
val responded_users_count: Int
)
```
---
## 🎯 ЭТАП 4: СОЗДАНИЕ UI КОМПОНЕНТОВ
### 4.1 Emergency Fragment/Activity
```kotlin
class EmergencyFragment : Fragment(), EmergencyWebSocketListener {
private lateinit var binding: FragmentEmergencyBinding
private lateinit var emergencyRepository: EmergencyRepository
private lateinit var webSocketManager: EmergencyWebSocketManager
private lateinit var viewModel: EmergencyViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentEmergencyBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWebSocket()
setupUI()
observeViewModel()
}
private fun setupWebSocket() {
webSocketManager.addListener(this)
webSocketManager.connect()
}
private fun setupUI() {
binding.btnCreateAlert.setOnClickListener {
showCreateAlertDialog()
}
binding.btnSafetyCheck.setOnClickListener {
createSafetyCheck()
}
binding.btnViewNearby.setOnClickListener {
loadNearbyAlerts()
}
binding.swipeRefresh.setOnRefreshListener {
refreshData()
}
}
private fun showCreateAlertDialog() {
val dialog = CreateAlertDialog { alertRequest ->
viewModel.createAlert(alertRequest)
}
dialog.show(parentFragmentManager, "CREATE_ALERT")
}
private fun createSafetyCheck() {
locationManager.getCurrentLocation { location ->
val safetyCheck = SafetyCheckRequest(
latitude = location.latitude,
longitude = location.longitude,
status = "safe",
message = "Regular safety check"
)
viewModel.createSafetyCheck(safetyCheck)
}
}
// WebSocket Listener методы
override fun onConnectionEstablished(userId: Int) {
activity?.runOnUiThread {
binding.connectionStatus.text = "Connected (User: $userId)"
binding.connectionStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.green))
}
}
override fun onNewAlert(alert: EmergencyAlertResponse) {
activity?.runOnUiThread {
showNewAlertNotification(alert)
viewModel.refreshAlerts()
}
}
override fun onAlertUpdate(update: AlertUpdateData) {
activity?.runOnUiThread {
viewModel.updateAlert(update)
}
}
override fun onError(error: String) {
activity?.runOnUiThread {
binding.connectionStatus.text = "Error: $error"
binding.connectionStatus.setTextColor(ContextCompat.getColor(requireContext(), R.color.red))
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
}
override fun onDestroy() {
webSocketManager.removeListener(this)
super.onDestroy()
}
}
```
### 4.2 Emergency ViewModel
```kotlin
class EmergencyViewModel(
private val repository: EmergencyRepository
) : ViewModel() {
private val _alerts = MutableLiveData<List<EmergencyAlertResponse>>()
val alerts: LiveData<List<EmergencyAlertResponse>> = _alerts
private val _nearbyAlerts = MutableLiveData<List<NearbyAlertResponse>>()
val nearbyAlerts: LiveData<List<NearbyAlertResponse>> = _nearbyAlerts
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun createAlert(request: CreateAlertRequest) {
viewModelScope.launch {
_isLoading.value = true
try {
val result = repository.createAlert(request)
if (result.isSuccess) {
// Обновляем список после создания
loadMyAlerts()
} else {
_error.value = "Failed to create alert: ${result.exceptionOrNull()?.message}"
}
} catch (e: Exception) {
_error.value = "Error creating alert: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun loadMyAlerts() {
viewModelScope.launch {
_isLoading.value = true
try {
val result = repository.getMyAlerts()
if (result.isSuccess) {
_alerts.value = result.getOrNull() ?: emptyList()
} else {
_error.value = "Failed to load alerts: ${result.exceptionOrNull()?.message}"
}
} catch (e: Exception) {
_error.value = "Error loading alerts: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun loadNearbyAlerts(latitude: Double, longitude: Double, radius: Int = 5) {
viewModelScope.launch {
_isLoading.value = true
try {
val result = repository.getNearbyAlerts(latitude, longitude, radius)
if (result.isSuccess) {
_nearbyAlerts.value = result.getOrNull() ?: emptyList()
} else {
_error.value = "Failed to load nearby alerts: ${result.exceptionOrNull()?.message}"
}
} catch (e: Exception) {
_error.value = "Error loading nearby alerts: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun respondToAlert(alertId: Int, responseType: String, message: String? = null) {
viewModelScope.launch {
_isLoading.value = true
try {
val request = AlertResponseRequest(responseType, message)
val result = repository.respondToAlert(alertId, request)
if (result.isSuccess) {
// Обновляем список после ответа
loadMyAlerts()
} else {
_error.value = "Failed to respond to alert: ${result.exceptionOrNull()?.message}"
}
} catch (e: Exception) {
_error.value = "Error responding to alert: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun createSafetyCheck(request: SafetyCheckRequest) {
viewModelScope.launch {
try {
val result = repository.createSafetyCheck(request)
if (result.isFailure) {
_error.value = "Failed to create safety check: ${result.exceptionOrNull()?.message}"
}
} catch (e: Exception) {
_error.value = "Error creating safety check: ${e.message}"
}
}
}
fun updateAlert(update: AlertUpdateData) {
val currentAlerts = _alerts.value?.toMutableList() ?: return
val index = currentAlerts.indexOfFirst { it.id == update.alert_id }
if (index != -1) {
currentAlerts[index] = currentAlerts[index].copy(
status = update.status,
responded_users_count = update.responded_users_count
)
_alerts.value = currentAlerts
}
}
}
```
### 4.3 Emergency Repository
```kotlin
class EmergencyRepository(
private val apiService: EmergencyApiService,
private val authManager: AuthManager
) {
private fun getAuthHeader(): String {
val token = authManager.getValidJwtToken()
return "Bearer $token"
}
suspend fun createAlert(request: CreateAlertRequest): Result<EmergencyAlertResponse> {
return try {
val response = apiService.createAlert(request, getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getMyAlerts(): Result<List<EmergencyAlertResponse>> {
return try {
val response = apiService.getMyAlerts(getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getActiveAlerts(): Result<List<EmergencyAlertResponse>> {
return try {
val response = apiService.getActiveAlerts(getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getNearbyAlerts(latitude: Double, longitude: Double, radius: Int): Result<List<NearbyAlertResponse>> {
return try {
val response = apiService.getNearbyAlerts(latitude, longitude, radius, getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun respondToAlert(alertId: Int, request: AlertResponseRequest): Result<EmergencyResponseResponse> {
return try {
val response = apiService.respondToAlert(alertId, request, getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createReport(request: CreateReportRequest): Result<EmergencyReportResponse> {
return try {
val response = apiService.createReport(request, getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createSafetyCheck(request: SafetyCheckRequest): Result<SafetyCheckResponse> {
return try {
val response = apiService.createSafetyCheck(request, getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getStatistics(): Result<EmergencyStatistics> {
return try {
val response = apiService.getStatistics(getAuthHeader())
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
```
---
## 🔧 ЭТАП 5: КОНФИГУРАЦИЯ И НАСТРОЙКА
### 5.1 Network Configuration
```kotlin
object NetworkConfig {
const val BASE_URL = "http://YOUR_SERVER_IP:8000/"
const val EMERGENCY_URL = "http://YOUR_SERVER_IP:8002/"
const val WS_URL = "ws://YOUR_SERVER_IP:8002/"
const val CONNECT_TIMEOUT = 30L
const val READ_TIMEOUT = 30L
const val WRITE_TIMEOUT = 30L
}
// Retrofit setup
val retrofit = Retrofit.Builder()
.baseUrl(NetworkConfig.BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.client(
OkHttpClient.Builder()
.connectTimeout(NetworkConfig.CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(NetworkConfig.READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(NetworkConfig.WRITE_TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(AuthInterceptor(authManager))
.build()
)
.build()
class AuthInterceptor(private val authManager: AuthManager) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// Добавляем JWT токен к запросам, если он есть
val token = authManager.getValidJwtToken()
return if (token != null && !originalRequest.header("Authorization")?.startsWith("Bearer") == true) {
val newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}
```
### 5.2 Dependency Injection (Hilt/Dagger)
```kotlin
@Module
@InstallIn(SingletonComponent::class)
object EmergencyModule {
@Provides
@Singleton
fun provideAuthManager(@ApplicationContext context: Context): AuthManager {
return AuthManager(context)
}
@Provides
@Singleton
fun provideEmergencyApiService(): EmergencyApiService {
return retrofit.create(EmergencyApiService::class.java)
}
@Provides
@Singleton
fun provideEmergencyRepository(
apiService: EmergencyApiService,
authManager: AuthManager
): EmergencyRepository {
return EmergencyRepository(apiService, authManager)
}
@Provides
@Singleton
fun provideEmergencyWebSocketManager(
authManager: AuthManager,
@ApplicationScope scope: CoroutineScope
): EmergencyWebSocketManager {
return EmergencyWebSocketManager(authManager, scope)
}
}
@ViewModelScope
class EmergencyViewModel @Inject constructor(
private val repository: EmergencyRepository
) : ViewModel()
```
---
## 🧪 ЭТАП 6: ТЕСТИРОВАНИЕ И ОТЛАДКА
### 6.1 Unit Tests
```kotlin
class EmergencyRepositoryTest {
@Mock
private lateinit var apiService: EmergencyApiService
@Mock
private lateinit var authManager: AuthManager
private lateinit var repository: EmergencyRepository
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
repository = EmergencyRepository(apiService, authManager)
}
@Test
fun `createAlert should return success when API call succeeds`() = runTest {
// Arrange
val request = CreateAlertRequest("medical", 55.7558, 37.6176, null, "Test alert")
val response = EmergencyAlertResponse(1, "medical", 55.7558, 37.6176, null, "Test alert", "active", "2025-10-18T00:00:00Z", 0)
`when`(authManager.getValidJwtToken()).thenReturn("valid_token")
`when`(apiService.createAlert(any(), any())).thenReturn(Response.success(response))
// Act
val result = repository.createAlert(request)
// Assert
assertTrue(result.isSuccess)
assertEquals(response, result.getOrNull())
}
@Test
fun `createAlert should return failure when API call fails`() = runTest {
// Arrange
val request = CreateAlertRequest("medical", 55.7558, 37.6176, null, "Test alert")
`when`(authManager.getValidJwtToken()).thenReturn("valid_token")
`when`(apiService.createAlert(any(), any())).thenReturn(Response.error(500, "".toResponseBody()))
// Act
val result = repository.createAlert(request)
// Assert
assertTrue(result.isFailure)
}
}
```
### 6.2 Integration Tests
```kotlin
class EmergencyIntegrationTest {
private lateinit var webSocketManager: EmergencyWebSocketManager
private lateinit var repository: EmergencyRepository
@Test
fun `should connect to WebSocket and receive messages`() = runTest {
// Этот тест требует запущенного сервера
val authManager = TestAuthManager() // Мок с валидным токеном
webSocketManager = EmergencyWebSocketManager(authManager, this)
var connectionEstablished = false
var messageReceived = false
webSocketManager.addListener(object : EmergencyWebSocketListener {
override fun onConnectionEstablished(userId: Int) {
connectionEstablished = true
}
override fun onNewAlert(alert: EmergencyAlertResponse) {
messageReceived = true
}
// Другие методы...
})
webSocketManager.connect()
// Ждем подключения
delay(5000)
assertTrue("WebSocket connection should be established", connectionEstablished)
}
}
```
---
## 📱 ЭТАП 7: UI/UX УЛУЧШЕНИЯ
### 7.1 Push Notifications
```kotlin
class EmergencyNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
val data = remoteMessage.data
when (data["type"]) {
"emergency_alert" -> {
showEmergencyAlertNotification(data)
}
"alert_response" -> {
showAlertResponseNotification(data)
}
"safety_check_reminder" -> {
showSafetyCheckReminder()
}
}
}
private fun showEmergencyAlertNotification(data: Map<String, String>) {
val alertType = data["alert_type"] ?: "emergency"
val location = data["address"] ?: "Unknown location"
val notification = NotificationCompat.Builder(this, EMERGENCY_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_emergency)
.setContentTitle("🚨 Emergency Alert: ${alertType.uppercase()}")
.setContentText("Emergency situation reported near $location")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setAutoCancel(false)
.addAction(R.drawable.ic_help, "Respond", createRespondPendingIntent(data["alert_id"]))
.addAction(R.drawable.ic_view, "View Details", createViewPendingIntent(data["alert_id"]))
.build()
NotificationManagerCompat.from(this).notify(EMERGENCY_NOTIFICATION_ID, notification)
}
}
```
### 7.2 Location Services Integration
```kotlin
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
@SuppressLint("MissingPermission")
fun getCurrentLocation(callback: (Location) -> Unit) {
if (hasLocationPermission()) {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
callback(location)
} else {
requestNewLocationData(callback)
}
}
} else {
// Request location permissions
}
}
@SuppressLint("MissingPermission")
private fun requestNewLocationData(callback: (Location) -> Unit) {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 0
fastestInterval = 0
numUpdates = 1
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { callback(it) }
}
},
Looper.myLooper()
)
}
private fun hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
}
```
---
## 🔒 ЭТАП 8: БЕЗОПАСНОСТЬ И ОПТИМИЗАЦИЯ
### 8.1 Security Best Practices
```kotlin
object SecurityManager {
// Шифрование токенов в SharedPreferences
fun saveEncryptedToken(context: Context, token: String) {
val sharedPrefs = EncryptedSharedPreferences.create(
"secure_prefs",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPrefs.edit()
.putString("jwt_token", token)
.apply()
}
// Certificate Pinning
fun createSecureOkHttpClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add("your-server.com", "sha256/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
// Защита от Man-in-the-Middle атак
fun validateServerCertificate(hostname: String): Boolean {
// Implement certificate validation logic
return true
}
}
```
### 8.2 Performance Optimization
```kotlin
class EmergencyDataCache @Inject constructor() {
private val alertsCache = LruCache<String, List<EmergencyAlertResponse>>(50)
private val cacheExpiry = mutableMapOf<String, Long>()
private val cacheTimeout = 5 * 60 * 1000L // 5 minutes
fun cacheAlerts(key: String, alerts: List<EmergencyAlertResponse>) {
alertsCache.put(key, alerts)
cacheExpiry[key] = System.currentTimeMillis() + cacheTimeout
}
fun getCachedAlerts(key: String): List<EmergencyAlertResponse>? {
val expiry = cacheExpiry[key] ?: return null
return if (System.currentTimeMillis() < expiry) {
alertsCache.get(key)
} else {
alertsCache.remove(key)
cacheExpiry.remove(key)
null
}
}
}
```
---
## 📋 ЭТАП 9: ЧЕКЛИСТ ГОТОВНОСТИ
### ✅ Обязательные компоненты:
- [ ] Удалены все `temp_token_` токены
- [ ] Реализована JWT аутентификация
- [ ] WebSocket подключение с правильным URL
- [ ] REST API интеграция для всех endpoint'ов
- [ ] Обработка ошибок сети и аутентификации
- [ ] Push уведомления для экстренных сообщений
- [ ] Location services для определения координат
- [ ] UI для создания и просмотра экстренных вызовов
### ✅ Тестирование:
- [ ] Unit тесты для Repository и ViewModel
- [ ] Integration тесты WebSocket подключения
- [ ] UI тесты для критических сценариев
- [ ] Тесты на различных сетевых условиях
- [ ] Тесты безопасности токенов
### ✅ Безопасность:
- [ ] Шифрование токенов в хранилище
- [ ] Certificate pinning
- [ ] Проверка SSL сертификатов
- [ ] Обфускация кода
- [ ] Защита от reverse engineering
### ✅ Производительность:
- [ ] Кеширование данных
- [ ] Оптимизация изображений
- [ ] Минификация сетевых запросов
- [ ] Фоновая синхронизация
- [ ] Battery optimization
---
## 🚀 ЭТАП 10: РАЗВЕРТЫВАНИЕ
### 10.1 Build Configuration
```gradle
android {
buildTypes {
debug {
buildConfigField "String", "BASE_URL", "\"http://192.168.1.100:8000/\""
buildConfigField "String", "WS_URL", "\"ws://192.168.1.100:8002/\""
}
release {
buildConfigField "String", "BASE_URL", "\"https://your-production-server.com/\""
buildConfigField "String", "WS_URL", "\"wss://your-production-server.com/\""
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
```
### 10.2 Monitoring and Analytics
```kotlin
class EmergencyAnalytics @Inject constructor() {
fun trackEmergencyAlertCreated(alertType: String) {
FirebaseAnalytics.getInstance(context).logEvent("emergency_alert_created") {
param("alert_type", alertType)
param("timestamp", System.currentTimeMillis())
}
}
fun trackWebSocketConnection(success: Boolean, errorMessage: String? = null) {
FirebaseAnalytics.getInstance(context).logEvent("websocket_connection") {
param("success", success)
errorMessage?.let { param("error", it) }
}
}
fun trackResponseTime(endpoint: String, responseTime: Long) {
FirebaseAnalytics.getInstance(context).logEvent("api_response_time") {
param("endpoint", endpoint)
param("response_time_ms", responseTime)
}
}
}
```
---
## 📞 КОНТАКТЫ И ПОДДЕРЖКА
**Сервер endpoints для тестирования:**
- API Gateway: `http://YOUR_SERVER:8000`
- Emergency Service: `http://YOUR_SERVER:8002`
- WebSocket: `ws://YOUR_SERVER:8002/api/v1/emergency/ws/current_user_id?token=JWT_TOKEN`
**Тестовые данные:**
- Email: `shadow85@list.ru`
- Password: `R0sebud1985`
**Документация API:** `/home/data/chat/docs/WEBSOCKET_AUTH_EXPLANATION.md`
---
Этот промпт содержит полное пошаговое техническое задание для интеграции мобильного приложения с Emergency Service. Следуйте этапам последовательно для успешной реализации! 🚀