package com.grigowashere.aismap; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Looper; import android.util.Log; import android.util.Printer; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import android.view.ViewGroup; import android.graphics.Color; import android.view.WindowManager; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.grigowashere.aismap.controllers.AppController; import com.grigowashere.aismap.controllers.MapController; import com.grigowashere.aismap.controllers.VesselPathController; import com.grigowashere.aismap.maps.MapInterface; import com.grigowashere.aismap.models.Vessel; import com.grigowashere.aismap.models.AISVessel; import com.grigowashere.aismap.sensors.CompassSensor; import com.grigowashere.aismap.view.CompassView; import com.grigowashere.aismap.view.CoordinatesDockWidget; import com.grigowashere.aismap.view.BaseDockWidget; import com.grigowashere.aismap.utils.SettingsManager; import com.grigowashere.aismap.utils.LogSender; import com.grigowashere.aismap.utils.MIDToCountry; import com.grigowashere.aismap.ui.UIRenderingCoordinator; import com.grigowashere.aismap.ui.UIDataChangeNotifier; // import com.yandex.mapkit.mapview.MapView; import org.maplibre.android.maps.MapView; import org.maplibre.android.MapLibre; import java.util.List; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private static final int PERMISSION_REQUEST_CODE = 1001; private static final int SETTINGS_REQUEST_CODE = 1002; private static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 1003; // Статическая переменная для отслеживания инициализации Яндекс.Карт private static boolean isYandexMapsInitialized = false; private AppController appController; private MapController mapController; private MapInterface mapInterface; private UIRenderingCoordinator uiCoordinator; private MapView mapView; private SettingsManager settingsManager; private Button btnCenterOnVessel; private Button btnMapOrientation; private Button btnSettings; private Button btnAisTargets; private LinearLayout controlPanel; private CompassView compassView; private CompassSensor compassSensor; private CoordinatesDockWidget coordinatesWidget; // Троттлинг для UI обновлений private android.os.Handler uiThrottleHandler; private Runnable compassUpdateRunnable; private Runnable coordinatesUpdateRunnable; private Vessel lastCompassVessel; private Vessel lastCoordinatesVessel; private static final long UI_UPDATE_THROTTLE_MS = 200; // 5 FPS максимум private TextView tvGpsAge; private TextView tvAisAge; private android.os.Handler messageAgeHandler; private Runnable messageAgeRunnable; // BottomSheet для отображения информации о нашем судне private BottomSheetDialog ownVesselBottomSheet; private View bottomSheetView; // BottomSheet для отображения информации об AIS судне private BottomSheetDialog aisVesselBottomSheet; private View aisBottomSheetView; private AISVessel currentAISVessel; // Текущее AIS судно в BottomSheet private android.os.Handler timeUpdateHandler; // Handler для обновления времени private Runnable timeUpdateRunnable; // Runnable для обновления времени // Автоматическое обновление BottomSheet private android.os.Handler bottomSheetUpdateHandler; // Handler для обновления BottomSheet private Runnable bottomSheetUpdateRunnable; // Runnable для обновления BottomSheet private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; // Обновление каждую секунду // Отложенное центрирование из внешнего интента private Double pendingCenterLat = null; private Double pendingCenterLon = null; // Управление экраном private boolean keepScreenOn = true; // UI Watchdog для отслеживания зависаний private android.os.Handler uiWatchdogHandler; private Runnable uiWatchdogRunnable; private long lastUIUpdateTime = 0; private static final long UI_WATCHDOG_INTERVAL = 1000; // 1 секунда - быстрая диагностика private static final long UI_TIMEOUT = 3000; // 3 секунды без обновлений = зависание // Диагностика компаса private long lastCompassLogTime = 0; private long lastTouchLogTime = 0; private long lastKeyLogTime = 0; // Throttling для updateControlPanelPosition private android.os.Handler controlPanelUpdateHandler; private Runnable controlPanelUpdateRunnable; private boolean controlPanelUpdatePending = false; private static final long CONTROL_PANEL_UPDATE_DELAY = 200; // 200ms throttling private int controlPanelUpdateCount = 0; // Для диагностики private long lastControlPanelUpdateTime = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Отключено: агрессивная диагностика StrictMode/Looper // Инициализация MapLibre перед созданием MapView try { MapLibre.getInstance(this); } catch (Exception e) { Log.e(TAG, "Ошибка инициализации MapLibre: " + e.getMessage(), e); } setContentView(R.layout.activity_main); initializeViews(); initializeControllers(); setupScreenManagement(); setupUIWatchdog(); // checkPermissions() будет вызван в onStart } // Отключено: принудительное bringToFront панели // Отключено: дополнительное логирование событий ввода private void initializeViews() { mapView = findViewById(R.id.map_view); btnCenterOnVessel = findViewById(R.id.btn_center_vessel); btnMapOrientation = findViewById(R.id.btn_map_orientation); btnSettings = findViewById(R.id.btn_settings); btnAisTargets = findViewById(R.id.btn_ais_targets); controlPanel = findViewById(R.id.control_panel); compassView = findViewById(R.id.compass_view); coordinatesWidget = findViewById(R.id.coordinates_widget); // Инициализируем троттлинг uiThrottleHandler = new android.os.Handler(android.os.Looper.getMainLooper()); compassUpdateRunnable = () -> { if (compassView != null && lastCompassVessel != null) { compassView.setOurVessel(lastCompassVessel); } }; coordinatesUpdateRunnable = () -> { if (coordinatesWidget != null && lastCoordinatesVessel != null) { coordinatesWidget.updateVessel(lastCoordinatesVessel); } }; tvGpsAge = findViewById(R.id.tv_gps_age); tvAisAge = findViewById(R.id.tv_ais_age); // Инициализируем магнитный компас compassSensor = new CompassSensor(this); // Инициализируем throttling для updateControlPanelPosition setupControlPanelThrottling(); initializeBottomSheet(); setupButtonListeners(); setupCompass(); setupCoordinatesWidget(); setupMessageAgesUpdater(); } private void setupButtonListeners() { btnCenterOnVessel.setOnClickListener(v -> centerOnVessel()); btnMapOrientation.setOnClickListener(v -> toggleMapOrientation()); btnSettings.setOnClickListener(v -> showSettings()); if (btnAisTargets != null) { btnAisTargets.setOnClickListener(v -> openAisTargets()); } // Кнопка для показа информации о судне // Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info); // if (btnShowVesselInfo != null) { // btnShowVesselInfo.setOnClickListener(v -> showOwnVesselBottomSheet()); // } } private void setupCompass() { // Устанавливаем начальный азимут (например, север) compassView.setAzimuth(0); // Устанавливаем компас в dock-режим вверху экрана compassView.post(() -> { compassView.setDocked(true, true, 0, 0); compassView.invalidate(); // Принудительная отрисовка }); // Настраиваем слушатель изменения размера док-виджета compassView.setOnDockResizeListener(newHeight -> { Log.d(TAG, "Compass dock height changed to: " + newHeight); // Обновляем позицию панели управления при любом изменении размера docked виджета updateControlPanelPosition(); }); // Настраиваем слушатель изменения состояния docked compassView.setOnDockStateChangeListener((isDocked, isTop) -> { Log.d(TAG, "Compass dock state changed: docked=" + isDocked + ", top=" + isTop); // Перепозиционируем все docked виджеты BaseDockWidget.repositionAllDockedWidgets((ViewGroup) compassView.getParent()); updateControlPanelPosition(); }); //smt changed // Настраиваем магнитный компас if (compassSensor.isAvailable()) { compassSensor.startListening(new CompassSensor.CompassListener() { //check how git is working @Override public void onCompassChanged(float azimuth) { // Диагностика: логируем каждые 10 секунд long now = System.currentTimeMillis(); if (now - lastCompassLogTime > 10000) { Log.d(TAG, "🧭 MainActivity: onCompassChanged получен, azimuth=" + azimuth); lastCompassLogTime = now; } // Обновляем компас в UI потоке runOnUiThread(() -> { // Диагностика: проверяем выполнение в UI потоке if (now - lastCompassLogTime > 10000) { Log.d(TAG, "🧭 MainActivity: runOnUiThread выполняется для компаса"); } compassView.setAzimuth(azimuth); compassView.setMagneticCompass(azimuth); // Обновляем магнитный компас в модели нашего судна if (appController != null) { Vessel ourVessel = appController.getOwnVessel(); if (ourVessel != null) { ourVessel.setMagneticCompass(azimuth); } } }); } }); Log.d(TAG, "Magnetic compass started"); } else { Log.w(TAG, "Magnetic compass not available"); } // Принудительная отрисовка compassView.invalidate(); // Инициализируем начальную позицию панели управления compassView.post(() -> { updateControlPanelPosition(); }); } private void setupCoordinatesWidget() { // Настраиваем слушатель изменения размера dock-виджета coordinatesWidget.setOnDockResizeListener(newHeight -> { Log.d(TAG, "Coordinates dock height changed to: " + newHeight); // Обновляем позицию панели управления при любом изменении размера docked виджета updateControlPanelPosition(); }); // Настраиваем слушатель изменения состояния docked coordinatesWidget.setOnDockStateChangeListener((isDocked, isTop) -> { Log.d(TAG, "Coordinates dock state changed: docked=" + isDocked + ", top=" + isTop); // Перепозиционируем все docked виджеты BaseDockWidget.repositionAllDockedWidgets((ViewGroup) coordinatesWidget.getParent()); updateControlPanelPosition(); }); // Устанавливаем виджет координат в dock-режим внизу экрана coordinatesWidget.post(() -> { Log.d(TAG, "Setting coordinates widget to dock mode"); coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу coordinatesWidget.invalidate(); // Принудительная отрисовка // Принудительно обновляем виджет с тестовыми данными (в фоне) android.os.Handler bgHandler = new android.os.Handler(android.os.Looper.getMainLooper()); bgHandler.post(() -> { try { Vessel testVessel = new Vessel(); testVessel.setLatitude(55.7558); testVessel.setLongitude(37.6176); testVessel.setSpeed(5.5); testVessel.setCourse(45.0); testVessel.setAccuracy(3.0f); coordinatesWidget.updateVessel(testVessel); // Используем throttled версию updateControlPanelPositionThrottled(); } catch (Exception e) { Log.e(TAG, "Ошибка при инициализации тестового виджета: " + e.getMessage(), e); } }); }); } private void setupMessageAgesUpdater() { messageAgeHandler = new android.os.Handler(android.os.Looper.getMainLooper()); messageAgeRunnable = new Runnable() { @Override public void run() { try { if (appController != null) { int gpsSec = appController.getSecondsSinceLastGPSMessage(); int aisSec = appController.getSecondsSinceLastAISMessage(); if (tvGpsAge != null) { tvGpsAge.setText(gpsSec >= 0 ? ("GPS: " + gpsSec + " сек назад") : "GPS: --"); tvGpsAge.setTextColor(getAgeColor(gpsSec)); } if (tvAisAge != null) { tvAisAge.setText(aisSec >= 0 ? ("AIS: " + aisSec + " сек назад") : "AIS: --"); tvAisAge.setTextColor(getAgeColor(aisSec)); } } } catch (Exception ignored) {} messageAgeHandler.postDelayed(this, 1000); } }; // Стартуем после первичной инициализации messageAgeHandler.postDelayed(messageAgeRunnable, 1000); } private int getAgeColor(int seconds) { if (seconds < 0) { // Нет данных return Color.parseColor("#F44336"); // красный } if (seconds < 30) { return Color.parseColor("#4CAF50"); // зелёный } else if (seconds < 300) { return Color.parseColor("#FFC107"); // жёлтый } else { return Color.parseColor("#F44336"); // красный } } private void onUpdateCompass(float azimuth, List nearbyVessels) { if (compassView != null) { compassView.setAzimuth(azimuth); compassView.updateNearbyVessels(nearbyVessels); } } /** * Инициализирует BottomSheet для отображения информации о нашем судне */ private void initializeBottomSheet() { // Инициализация Handler для обновления времени timeUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); timeUpdateRunnable = new Runnable() { @Override public void run() { if (currentAISVessel != null && aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing()) { updateAISTimeAgo(); } // Планируем следующее обновление через 1 секунду timeUpdateHandler.postDelayed(this, 1000); } }; // Инициализация Handler для автоматического обновления BottomSheet bottomSheetUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); bottomSheetUpdateRunnable = new Runnable() { @Override public void run() { // Обновляем BottomSheet нашего судна, если он открыт if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { updateBottomSheetUI(); } // Обновляем AIS BottomSheet, если он открыт if (aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing() && currentAISVessel != null) { updateAISBottomSheetUI(currentAISVessel); } // Планируем следующее обновление bottomSheetUpdateHandler.postDelayed(this, BOTTOM_SHEET_UPDATE_INTERVAL); } }; // Инициализация BottomSheet для нашего судна ownVesselBottomSheet = new BottomSheetDialog(this); bottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_own_vessel, null); ownVesselBottomSheet.setContentView(bottomSheetView); // Настраиваем кнопку закрытия ImageButton btnClose = bottomSheetView.findViewById(R.id.btn_close_bottom_sheet); btnClose.setOnClickListener(v -> { ownVesselBottomSheet.dismiss(); // Восстанавливаем обработчики кликов после закрытия restoreMarkerClickListeners(); // Останавливаем автоматическое обновление stopBottomSheetAutoUpdate(); }); // Настраиваем поведение BottomSheet ownVesselBottomSheet.setCanceledOnTouchOutside(true); ownVesselBottomSheet.setCancelable(true); // Добавляем слушатель закрытия BottomSheet ownVesselBottomSheet.setOnDismissListener(dialog -> { // Восстанавливаем обработчики кликов после закрытия restoreMarkerClickListeners(); // Останавливаем автоматическое обновление stopBottomSheetAutoUpdate(); }); // Инициализация BottomSheet для AIS судов aisVesselBottomSheet = new BottomSheetDialog(this); aisBottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_ais_vessel, null); aisVesselBottomSheet.setContentView(aisBottomSheetView); // Настраиваем кнопку закрытия для AIS BottomSheet ImageButton btnCloseAIS = aisBottomSheetView.findViewById(R.id.btn_close_ais_bottom_sheet); btnCloseAIS.setOnClickListener(v -> { aisVesselBottomSheet.dismiss(); stopTimeUpdate(); // Восстанавливаем обработчики кликов после закрытия restoreMarkerClickListeners(); // Останавливаем автоматическое обновление stopBottomSheetAutoUpdate(); }); // Настраиваем поведение AIS BottomSheet aisVesselBottomSheet.setCanceledOnTouchOutside(true); aisVesselBottomSheet.setCancelable(true); // Добавляем слушатель закрытия BottomSheet aisVesselBottomSheet.setOnDismissListener(dialog -> { stopTimeUpdate(); // Восстанавливаем обработчики кликов после закрытия restoreMarkerClickListeners(); // Останавливаем автоматическое обновление stopBottomSheetAutoUpdate(); }); } /** * Настраивает управление экраном */ private void setupScreenManagement() { // Загружаем настройку из SettingsManager if (settingsManager != null) { keepScreenOn = settingsManager.isKeepScreenOnEnabled(); } // Применяем настройку setKeepScreenOn(keepScreenOn); Log.i(TAG, "Управление экраном настроено: keepScreenOn=" + keepScreenOn); } /** * Настраивает UI watchdog для отслеживания зависаний */ private void setupUIWatchdog() { uiWatchdogHandler = new android.os.Handler(android.os.Looper.getMainLooper()); uiWatchdogRunnable = new Runnable() { @Override public void run() { long currentTime = System.currentTimeMillis(); long timeSinceLastUpdate = currentTime - lastUIUpdateTime; if (timeSinceLastUpdate > UI_TIMEOUT) { Log.e(TAG, "🚨 UI WATCHDOG: UI ЗАВИС! Последнее обновление " + (timeSinceLastUpdate / 1000) + " секунд назад"); Log.e(TAG, "🚨 UI WATCHDOG: Время зависания: " + new java.util.Date(currentTime)); Log.e(TAG, "🚨 UI WATCHDOG: Thread: " + Thread.currentThread().getName()); // Дамп стека главного потока и нескольких рабочих потоков dumpThreadStacksForDiagnostics(); // Попытка восстановления tryRecoverFromUIHang(); } else { // Логируем каждые 10 секунд для мониторинга if (timeSinceLastUpdate > 0 && (timeSinceLastUpdate / 1000) % 10 == 0) { Log.i(TAG, "✅ UI WATCHDOG: UI активен, последнее обновление " + (timeSinceLastUpdate / 1000) + " секунд назад"); } } // Планируем следующую проверку uiWatchdogHandler.postDelayed(this, UI_WATCHDOG_INTERVAL); } }; // Запускаем watchdog lastUIUpdateTime = System.currentTimeMillis(); uiWatchdogHandler.postDelayed(uiWatchdogRunnable, UI_WATCHDOG_INTERVAL); Log.i(TAG, "UI watchdog запущен"); } /** * Обновляет время последней активности UI */ private void updateUIActivity() { long now = System.currentTimeMillis(); long timeSinceLastUpdate = now - lastUIUpdateTime; lastUIUpdateTime = now; // Логируем если прошло больше 2 секунд с последнего обновления if (timeSinceLastUpdate > 2000) { Log.w(TAG, "⚠️ UI WATCHDOG: Долгая пауза в UI обновлениях: " + (timeSinceLastUpdate / 1000) + " сек"); } } /** * Попытка восстановления после зависания UI */ private void tryRecoverFromUIHang() { Log.w(TAG, "UI WATCHDOG: Попытка восстановления..."); try { // Диагностика: проверяем состояние handler'ов boolean watchdogActive = uiWatchdogHandler != null && uiWatchdogRunnable != null; boolean messageAgeActive = messageAgeHandler != null && messageAgeRunnable != null; boolean bottomSheetActive = bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null; boolean controlPanelActive = controlPanelUpdateHandler != null && controlPanelUpdateRunnable != null; Log.i(TAG, "UI WATCHDOG: Handler status - " + "watchdog=" + watchdogActive + ", messageAge=" + messageAgeActive + ", bottomSheet=" + bottomSheetActive + ", controlPanel=" + controlPanelActive + ", controlPanelCount=" + controlPanelUpdateCount); // Принудительная сборка мусора System.gc(); // Проверяем состояние основных компонентов if (mapInterface == null) { Log.w(TAG, "UI WATCHDOG: mapInterface == null, переинициализируем карту"); // Можно попробовать переинициализировать карту } if (appController == null) { Log.w(TAG, "UI WATCHDOG: appController == null"); } // Если слишком много обновлений control panel, попробуем остановить if (controlPanelUpdateCount > 50) { Log.w(TAG, "UI WATCHDOG: Слишком много обновлений control panel (" + controlPanelUpdateCount + "/10сек), принудительно останавливаем"); if (controlPanelUpdateHandler != null) { controlPanelUpdateHandler.removeCallbacks(controlPanelUpdateRunnable); controlPanelUpdatePending = false; controlPanelUpdateCount = 0; } } // Обновляем время активности updateUIActivity(); Log.i(TAG, "UI WATCHDOG: Восстановление завершено"); } catch (Exception e) { Log.e(TAG, "UI WATCHDOG: Ошибка при восстановлении: " + e.getMessage(), e); } } /** * Диагностический дамп стеков главного и рабочих потоков */ private void dumpThreadStacksForDiagnostics() { try { java.util.Map all = Thread.getAllStackTraces(); Thread main = Looper.getMainLooper().getThread(); // Сначала главный поток if (main != null) { StackTraceElement[] st = all.get(main); Log.e(TAG, "===== MAIN THREAD STACK TRACE ====="); if (st != null) { for (StackTraceElement e : st) { Log.e(TAG, " at " + e.toString()); } } } // Затем несколько самых активных потоков по имени String[] interesting = new String[] {"AsyncTask", "RenderThread", "OkHttp", "GLThread", "pool-", "DefaultDispatcher"}; for (java.util.Map.Entry entry : all.entrySet()) { Thread t = entry.getKey(); if (t == main) continue; String name = t.getName(); boolean match = false; for (String key : interesting) { if (name.contains(key)) { match = true; break; } } if (!match) continue; Log.w(TAG, "===== THREAD: " + name + " (" + t.getState() + ") ====="); StackTraceElement[] st = entry.getValue(); if (st != null) { int count = 0; for (StackTraceElement e : st) { Log.w(TAG, " at " + e.toString()); if (++count > 50) break; // ограничим длину } } } } catch (Throwable t) { Log.e(TAG, "Ошибка дампа стеков: " + t.getMessage(), t); } } /** * Настраивает throttling для updateControlPanelPosition */ private void setupControlPanelThrottling() { controlPanelUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); controlPanelUpdateRunnable = () -> { controlPanelUpdatePending = false; updateControlPanelPositionSafe(); }; } /** * Безопасное обновление позиции панели управления с throttling */ private void updateControlPanelPositionThrottled() { if (!controlPanelUpdatePending) { controlPanelUpdatePending = true; controlPanelUpdateHandler.removeCallbacks(controlPanelUpdateRunnable); controlPanelUpdateHandler.postDelayed(controlPanelUpdateRunnable, CONTROL_PANEL_UPDATE_DELAY); } } /** * Устанавливает режим работы экрана */ private void setKeepScreenOn(boolean enabled) { if (enabled) { // Включаем режим "не засыпать" getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Log.i(TAG, "Экран настроен на постоянную работу"); } else { // Выключаем режим "не засыпать" getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Log.i(TAG, "Экран настроен на обычный режим (может засыпать)"); } } /** * Переключает режим работы экрана */ public void toggleKeepScreenOn() { keepScreenOn = !keepScreenOn; setKeepScreenOn(keepScreenOn); // Сохраняем настройку if (settingsManager != null) { settingsManager.setKeepScreenOnEnabled(keepScreenOn); } String message = keepScreenOn ? "Экран будет оставаться включенным" : "Экран может засыпать"; Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); Log.i(TAG, "Режим экрана переключен: keepScreenOn=" + keepScreenOn); } private void initializeControllers() { // Инициализация менеджера настроек settingsManager = new SettingsManager(this); // Инициализация главного контроллера appController = new AppController(this); // Инициализация контроллера карты mapController = new MapController(this); // Устанавливаем callback для обновления UI // Запускаем Foreground Service для фоновых обновлений AIS/GPS startForegroundService(); appController.setUIUpdateCallback(new AppController.ExtendedUIUpdateCallback() { @Override public void onVesselPositionUpdated(Vessel vessel) { updateUIActivity(); // Обновляем watchdog updateVesselPositionUI(vessel); // Троттлинг обновлений компаса lastCompassVessel = vessel; uiThrottleHandler.removeCallbacks(compassUpdateRunnable); uiThrottleHandler.postDelayed(compassUpdateRunnable, UI_UPDATE_THROTTLE_MS); } @Override public void onGPSQualityUpdated(Vessel vessel) { updateGPSQualityUI(vessel); } @Override public void onShowOwnVesselBottomSheet() { Log.i(TAG, "onShowOwnVesselBottomSheet callback получен в MainActivity"); showOwnVesselBottomSheet(); } @Override public void onShowAISVesselInfo(AISVessel vessel) { showAISVesselBottomSheet(vessel); } @Override public void onUpdateCompass(float azimuth, List nearbyVessels) { updateUIActivity(); // Обновляем watchdog if (compassView != null) { compassView.setAzimuth(azimuth); compassView.updateNearbyVessels(nearbyVessels); } } }); } private void startControllers() { // Загружаем настройки и применяем их applySettings(); // Запускаем все слушатели appController.startAllListeners(); } /** * Обновляет статус в UI */ private void updateStatusUI() { // Обновляем статус в UI // TextView tvStatus = findViewById(R.id.tv_status); // TextView tvAisCount = findViewById(R.id.tv_ais_count); // // if (tvStatus != null) { // tvStatus.setText("Статус: GPS активен, UDP готов"); // } // // if (tvAisCount != null) { // tvAisCount.setText("AIS суда: 0"); // } } /** * Обновляет позицию судна в UI */ private void updateVesselPositionUI(Vessel vessel) { if (isFinishing() || isDestroyed()) return; runOnUiThread(() -> { try { updateUIActivity(); // Обновляем watchdog if (vessel == null) return; // Троттлинг обновлений координатного виджета lastCoordinatesVessel = vessel; uiThrottleHandler.removeCallbacks(coordinatesUpdateRunnable); uiThrottleHandler.postDelayed(coordinatesUpdateRunnable, UI_UPDATE_THROTTLE_MS); // Обновляем BottomSheet, если он открыт if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { updateBottomSheetUI(); } } catch (Exception e) { Log.e(TAG, "Ошибка в updateVesselPositionUI: " + e.getMessage(), e); } }); } /** * Обновляет качество GPS в UI */ private void updateGPSQualityUI(Vessel vessel) { if (isFinishing() || isDestroyed()) return; runOnUiThread(() -> { try { updateUIActivity(); // Обновляем watchdog if (vessel == null) return; // Обновляем BottomSheet, если он открыт if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { updateBottomSheetUI(); } } catch (Exception e) { Log.e(TAG, "Ошибка в updateGPSQualityUI: " + e.getMessage(), e); } }); } private void toggleUDP() { boolean isEnabled = appController.isUDPEnabled(); if (isEnabled) { appController.setUDPEnabled(false); Toast.makeText(this, "UDP слушатель отключен", Toast.LENGTH_SHORT).show(); } else { appController.setUDPEnabled(true); Toast.makeText(this, "UDP слушатель включен", Toast.LENGTH_SHORT).show(); } // Обновляем заголовок меню invalidateOptionsMenu(); } private void toggleGPS() { boolean isEnabled = appController.isAndroidNMEAEnabled(); if (isEnabled) { appController.setAndroidNMEAEnabled(false); Toast.makeText(this, "GPS слушатель отключен", Toast.LENGTH_SHORT).show(); } else { appController.setAndroidNMEAEnabled(true); Toast.makeText(this, "GPS слушатель включен", Toast.LENGTH_SHORT).show(); } // Обновляем заголовок меню invalidateOptionsMenu(); } private void centerOnVessel() { appController.centerOnOwnVessel(); Toast.makeText(this, "Карта центрирована на судне", Toast.LENGTH_SHORT).show(); } private void toggleMapOrientation() { // TODO: Реализовать переключение ориентации карты // Состояния: север, курс, компас Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show(); } private void togglePathTracking() { boolean currentState = settingsManager.isPathTrackingEnabled(); boolean newState = !currentState; settingsManager.setPathTrackingEnabled(newState); // Обновляем состояние в карте if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) { ((com.grigowashere.aismap.maps.YandexMapImpl) mapInterface).setPathTrackingEnabled(newState); } String message = newState ? "Отслеживание путей включено" : "Отслеживание путей выключено"; Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); // Обновляем меню invalidateOptionsMenu(); } /** * Очищает трекер пути собственного судна */ private void clearVesselPath() { try { Log.i(TAG, "clearVesselPath() вызван"); if (mapInterface != null) { Log.i(TAG, "Очищаем путь в карте"); // Очищаем путь в карте mapInterface.clearVesselPath(); // Также очищаем VesselPathController если он используется в AppController if (appController != null) { Log.i(TAG, "Очищаем VesselPathController в AppController"); appController.clearVesselPath(); } else { Log.w(TAG, "AppController is null, не можем очистить VesselPathController"); } Toast.makeText(this, "Трекер пути очищен", Toast.LENGTH_SHORT).show(); Log.i(TAG, "Трекер пути собственного судна очищен"); } else { Toast.makeText(this, "Карта не инициализирована", Toast.LENGTH_SHORT).show(); Log.w(TAG, "Попытка очистки пути при неинициализированной карте"); } } catch (Exception e) { Log.e(TAG, "Ошибка при очистке пути: " + e.getMessage(), e); Toast.makeText(this, "Ошибка при очистке пути", Toast.LENGTH_SHORT).show(); } } private void showSettings() { Intent intent = new Intent(this, SettingsActivity.class); startActivityForResult(intent, SETTINGS_REQUEST_CODE); } private void openAisTargets() { Intent intent = new Intent(this, AisTargetsActivity.class); startActivity(intent); } /** * Обновляет позицию панели управления с throttling */ private void updateControlPanelPosition() { updateControlPanelPositionThrottled(); } /** * Безопасное обновление позиции панели управления (вызывается через throttling) */ private void updateControlPanelPositionSafe() { if (controlPanel == null) return; try { updateUIActivity(); // Обновляем watchdog // Диагностика: считаем количество обновлений controlPanelUpdateCount++; long now = System.currentTimeMillis(); if (now - lastControlPanelUpdateTime > 10000) { // каждые 10 секунд Log.d(TAG, "Control panel updates count: " + controlPanelUpdateCount + " за последние 10 сек"); controlPanelUpdateCount = 0; lastControlPanelUpdateTime = now; } // Получаем параметры layout android.widget.RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams(); if (params == null) return; int defaultMargin = dpToPx(16); int topMargin = defaultMargin; int bottomMargin = defaultMargin; // Проверяем compassView int compassHeight = 0; if (compassView != null && compassView.isDocked()) { compassHeight = compassView.getHeight(); if (compassHeight <= 0) return; // Избегаем 0 размера, который может вызвать перестройку if (compassView.isDockTop()) { topMargin = compassHeight + dpToPx(8); } else { bottomMargin = compassHeight + dpToPx(8); } } // Проверяем coordinatesWidget int coordinatesHeight = 0; if (coordinatesWidget != null && coordinatesWidget.isDocked()) { coordinatesHeight = coordinatesWidget.getHeight(); if (coordinatesHeight <= 0) return; // Избегаем 0 размера if (coordinatesWidget.isDockTop()) { topMargin = Math.max(topMargin, coordinatesHeight + dpToPx(8)); } else { bottomMargin = Math.max(bottomMargin, coordinatesHeight + dpToPx(8)); } } // Применяем изменения только если они отличаются от текущих if (params.topMargin != topMargin || params.bottomMargin != bottomMargin) { params.topMargin = topMargin; params.bottomMargin = bottomMargin; controlPanel.setLayoutParams(params); // Минимальное логирование в production Log.d(TAG, "Control panel updated: top=" + topMargin + ", bottom=" + bottomMargin); } } catch (Exception e) { Log.e(TAG, "Ошибка при обновлении позиции панели управления: " + e.getMessage(), e); } } private void clearAIS() { appController.clearAISVessels(); Toast.makeText(this, "AIS суда очищены", Toast.LENGTH_SHORT).show(); } /** * Конвертирует dp в px */ private int dpToPx(int dp) { return (int) (dp * getResources().getDisplayMetrics().density); } @Override protected void onStart() { super.onStart(); // MapLibre lifecycle if (mapView != null) { mapView.onStart(); } // Запускаем карту через контроллер if (mapController != null) { Log.i(TAG, "Запускаем карту..."); mapController.startMap(); // Инициализируем карту Log.i(TAG, "Инициализируем карту..."); mapInterface = mapController.initializeMapLibre(mapView); Log.i(TAG, "mapInterface получен: " + (mapInterface != null ? "успешно" : "null")); // Устанавливаем интерфейс карты в главный контроллер if (mapInterface != null) { // Сначала создаем UI Coordinator uiCoordinator = new UIRenderingCoordinator(mapInterface); Log.i(TAG, "UIRenderingCoordinator создам"); // Устанавливаем UI Coordinator как notifier для AppController ДО setMapInterface appController.setUIDataChangeNotifier(uiCoordinator); Log.i(TAG, "UIDataChangeNotifier установлен в AppController"); // Теперь устанавливаем mapInterface - восстановление будет через uiDataNotifier Log.i(TAG, "Устанавливаем mapInterface в AppController..."); appController.setMapInterface(mapInterface); Log.i(TAG, "mapInterface установлен в AppController"); // Принудительно выполняем pending операции для восстановления данных uiCoordinator.flushPendingOperations(); Log.i(TAG, "Pending операции выполнены для восстановления маркеров"); // Инициализируем курсор согласно настройкам initializeCursor(); // Устанавливаем VesselPathController и AppController в MapController if (appController != null) { VesselPathController pathController = appController.getPathController(); if (pathController != null) { mapController.setVesselPathController(pathController); Log.i(TAG, "VesselPathController установлен в MapController"); } mapController.setAppController(appController); Log.i(TAG, "AppController установлен в MapController"); } mapInterface.initialize(); Log.i(TAG, "Карта инициализирована"); // Применяем отложенное центрирование, если было applyPendingCenterIfAny(); // Отслеживание путей для MapLibre будет добавлено позже // Проверяем, что все настроено правильно Log.i(TAG, "Проверяем настройку карты..."); // Дополнительная проверка обработчиков кликов Log.i(TAG, "Проверяем обработчики кликов..."); if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) { com.grigowashere.aismap.maps.YandexMapImpl yandexMap = (com.grigowashere.aismap.maps.YandexMapImpl) mapInterface; yandexMap.refreshMarkerClickListeners(); Log.i(TAG, "Обработчики кликов обновлены"); } } else { Log.e(TAG, "Не удалось получить mapInterface!"); } } // Обрабатываем возможный интент центрирования handleCenterIntentIfAny(getIntent()); // Проверяем разрешения и запускаем контроллеры checkPermissions(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); handleCenterIntentIfAny(intent); } private void handleCenterIntentIfAny(Intent intent) { if (intent == null) return; if (intent.hasExtra("center_lat") && intent.hasExtra("center_lon")) { double lat = intent.getDoubleExtra("center_lat", 0); double lon = intent.getDoubleExtra("center_lon", 0); Log.i(TAG, "Получен интент центрирования: lat=" + lat + ", lon=" + lon); if (lat != 0 || lon != 0) { if (mapInterface != null) { Log.i(TAG, "Центрируем карту немедленно"); mapInterface.centerOnPosition(lat, lon); } else { // Сохраняем для применения после инициализации карты Log.i(TAG, "Сохраняем координаты для отложенного центрирования"); pendingCenterLat = lat; pendingCenterLon = lon; } } // Сбрасываем, чтобы не повторялось при поворотах intent.removeExtra("center_lat"); intent.removeExtra("center_lon"); intent.removeExtra("center_mmsi"); } } private void applyPendingCenterIfAny() { if (mapInterface == null) return; if (pendingCenterLat != null && pendingCenterLon != null) { Log.i(TAG, "Применяем отложенное центрирование: lat=" + pendingCenterLat + ", lon=" + pendingCenterLon); mapInterface.centerOnPosition(pendingCenterLat, pendingCenterLon); pendingCenterLat = null; pendingCenterLon = null; } } @Override protected void onStop() { super.onStop(); // MapLibre lifecycle if (mapView != null) { mapView.onStop(); } // Останавливаем карту if (mapInterface != null) { mapInterface.cleanup(); } // Очищаем UI Coordinator if (uiCoordinator != null) { uiCoordinator.cleanup(); } // Очищаем троттлинг if (uiThrottleHandler != null) { uiThrottleHandler.removeCallbacks(compassUpdateRunnable); uiThrottleHandler.removeCallbacks(coordinatesUpdateRunnable); } // Не останавливаем слушатели здесь, чтобы UDP продолжал работать в фоне // if (appController != null) { // appController.stopAllListeners(); // } } @Override protected void onDestroy() { super.onDestroy(); // MapLibre lifecycle if (mapView != null) { mapView.onDestroy(); } // Останавливаем обновление времени stopTimeUpdate(); // Останавливаем автоматическое обновление BottomSheet stopBottomSheetAutoUpdate(); // Останавливаем обновление возраста сообщений if (messageAgeHandler != null && messageAgeRunnable != null) { messageAgeHandler.removeCallbacks(messageAgeRunnable); Log.i(TAG, "messageAgeHandler остановлен"); } // Останавливаем UI watchdog if (uiWatchdogHandler != null && uiWatchdogRunnable != null) { uiWatchdogHandler.removeCallbacks(uiWatchdogRunnable); Log.i(TAG, "UI watchdog остановлен"); } // Останавливаем throttling handler для control panel if (controlPanelUpdateHandler != null && controlPanelUpdateRunnable != null) { controlPanelUpdateHandler.removeCallbacks(controlPanelUpdateRunnable); Log.i(TAG, "Control panel throttling остановлен"); } // Останавливаем магнитный компас if (compassSensor != null) { compassSensor.stopListening(); } // Освобождаем ресурсы if (appController != null) { // Очищаем callback чтобы избежать утечки памяти appController.setUIUpdateCallback(null); appController.cleanup(); } if (mapController != null) { mapController.cleanup(); } // Останавливаем LogSender LogSender.shutdown(); // Останавливаем форграунд сервис stopForegroundService(); } @Override public void onConfigurationChanged(android.content.res.Configuration newConfig) { super.onConfigurationChanged(newConfig); // Обрабатываем изменения конфигурации (например, поворот экрана) if (mapInterface != null) { // Можно добавить логику для обработки изменений конфигурации карты } } /** * Проверяет необходимые разрешения */ private void checkPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_CODE); } else { // Разрешения уже получены, запускаем контроллеры startControllers(); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Разрешение получено, запускаем контроллеры startControllers(); } else { // Разрешение не получено Toast.makeText(this, "Для работы приложения необходимо разрешение на доступ к местоположению", Toast.LENGTH_LONG).show(); } } else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Разрешение на уведомления получено, запускаем сервис android.util.Log.i(TAG, "Разрешение на уведомления получено"); startForegroundService(); } else { // Разрешение на уведомления не получено android.util.Log.w(TAG, "Разрешение на уведомления не получено"); Toast.makeText(this, "Для работы в фоне необходимо разрешение на уведомления", Toast.LENGTH_LONG).show(); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SETTINGS_REQUEST_CODE) { if (resultCode == RESULT_OK && data != null) { boolean settingsChanged = data.getBooleanExtra("settings_changed", false); boolean needsRestart = data.getBooleanExtra("needs_restart", false); boolean clearVesselPath = data.getBooleanExtra("clear_vessel_path", false); boolean cursorEnabled = data.getBooleanExtra("cursor_enabled", false); if (clearVesselPath) { Log.i(TAG, "Запрошена очистка трекера пути"); clearVesselPath(); } if (settingsChanged) { Log.i(TAG, "Настройки изменены, применяем изменения"); // Применяем настройки курсора applyCursorSettings(cursorEnabled); if (needsRestart) { Log.i(TAG, "Требуется перезапуск сервисов"); restartServices(); } else { Log.i(TAG, "Применяем настройки без перезапуска"); applySettings(); } Toast.makeText(this, "Настройки применены", Toast.LENGTH_SHORT).show(); } } } } // Меню @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { // Обновляем состояние элементов меню MenuItem gpsItem = menu.findItem(R.id.menu_gps); MenuItem udpItem = menu.findItem(R.id.menu_udp); if (gpsItem != null) { gpsItem.setTitle(appController.isAndroidNMEAEnabled() ? "GPS ✓" : "GPS"); } if (udpItem != null) { udpItem.setTitle(appController.isUDPEnabled() ? "UDP ✓" : "UDP"); } MenuItem pathItem = menu.findItem(R.id.menu_path_tracking); if (pathItem != null) { boolean pathEnabled = settingsManager.isPathTrackingEnabled(); pathItem.setTitle(pathEnabled ? "Пути ✓" : "Пути"); } MenuItem screenItem = menu.findItem(R.id.menu_keep_screen_on); if (screenItem != null) { boolean screenEnabled = settingsManager.isKeepScreenOnEnabled(); screenItem.setTitle(screenEnabled ? "Экран ✓" : "Экран"); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_gps) { toggleGPS(); return true; } else if (id == R.id.menu_udp) { toggleUDP(); return true; } else if (id == R.id.menu_clear_ais) { clearAIS(); return true; } else if (id == R.id.menu_path_tracking) { togglePathTracking(); return true; } else if (id == R.id.menu_service_test) { testForegroundService(); return true; } else if (id == R.id.menu_keep_screen_on) { toggleKeepScreenOn(); return true; } return super.onOptionsItemSelected(item); } /** * Показывает BottomSheet с информацией о нашем судне */ private void showOwnVesselBottomSheet() { if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) { updateBottomSheetUI(); ownVesselBottomSheet.show(); // Запускаем автоматическое обновление BottomSheet startBottomSheetAutoUpdate(); } } /** * Обновляет UI BottomSheet с актуальными данными */ private void updateBottomSheetUI() { if (bottomSheetView == null) return; Vessel vessel = appController.getOwnVessel(); if (vessel == null) return; // Убеждаемся, что обновление происходит в главном потоке runOnUiThread(() -> { if (bottomSheetView == null) return; Vessel currentVessel = appController.getOwnVessel(); if (currentVessel == null) return; // Обновляем все поля в BottomSheet TextView tvStatus = bottomSheetView.findViewById(R.id.bottom_sheet_status); TextView tvPosition = bottomSheetView.findViewById(R.id.bottom_sheet_position); TextView tvCourse = bottomSheetView.findViewById(R.id.bottom_sheet_course); TextView tvSpeed = bottomSheetView.findViewById(R.id.bottom_sheet_speed); TextView tvAltitude = bottomSheetView.findViewById(R.id.bottom_sheet_altitude); TextView tvAccuracy = bottomSheetView.findViewById(R.id.bottom_sheet_accuracy); TextView tvGPSQuality = bottomSheetView.findViewById(R.id.bottom_sheet_gps_quality); TextView tvSatellites = bottomSheetView.findViewById(R.id.bottom_sheet_satellites); TextView tvDOP = bottomSheetView.findViewById(R.id.bottom_sheet_dop); TextView tvFixTime = bottomSheetView.findViewById(R.id.bottom_sheet_fix_time); TextView tvFixQuality = bottomSheetView.findViewById(R.id.bottom_sheet_fix_quality); // Статус if (tvStatus != null) { if (currentVessel.getLatitude() != 0 && currentVessel.getLongitude() != 0) { tvStatus.setText("Статус: GPS активен, данные получены"); } else { tvStatus.setText("Статус: Ожидание GPS данных..."); } } // Координаты if (tvPosition != null) { if (currentVessel.getLatitude() != 0 && currentVessel.getLongitude() != 0) { String positionText = String.format("📍 Координаты: %.6f, %.6f", currentVessel.getLatitude(), currentVessel.getLongitude()); tvPosition.setText(positionText); } else { tvPosition.setText("📍 Координаты: Не определены"); } } // Курс if (tvCourse != null) { if (currentVessel.getCourse() > 0) { String courseText = String.format("🧭 Курс: %.1f°", currentVessel.getCourse()); tvCourse.setText(courseText); } else { tvCourse.setText("🧭 Курс: --°"); } } // Скорость if (tvSpeed != null) { if (currentVessel.getSpeed() > 0) { String speedText = String.format("⚡ Скорость: %.1f узлов", currentVessel.getSpeed()); tvSpeed.setText(speedText); } else { tvSpeed.setText("⚡ Скорость: -- узлов"); } } // Высота if (tvAltitude != null) { if (currentVessel.getAltitude() != 0) { String altitudeText = String.format("🏔️ Высота: %.1f м", currentVessel.getAltitude()); tvAltitude.setText(altitudeText); } else { tvAltitude.setText("🏔️ Высота: -- м"); } } // Точность if (tvAccuracy != null) { if (currentVessel.getAccuracy() > 0) { String accuracyText = String.format("🎯 Точность: %.1f м", currentVessel.getAccuracy()); tvAccuracy.setText(accuracyText); } else { tvAccuracy.setText("🎯 Точность: -- м"); } } // Качество GPS if (tvGPSQuality != null) { if (currentVessel.getGPSQualityDescription() != null) { String qualityText = String.format("📊 Качество GPS: %s", currentVessel.getGPSQualityDescription()); tvGPSQuality.setText(qualityText); } else { tvGPSQuality.setText("📊 Качество GPS: --"); } } // Спутники if (tvSatellites != null) { if (currentVessel.getSatellites() > 0) { String satellitesText = String.format("Спутники: %d/%d", currentVessel.getActiveSatellites(), currentVessel.getSatellites()); tvSatellites.setText(satellitesText); } else { tvSatellites.setText("Спутники: --/--"); } } // DOP if (tvDOP != null) { if (currentVessel.getPdop() > 0) { String dopText = String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", currentVessel.getPdop(), currentVessel.getHdop(), currentVessel.getVdop()); tvDOP.setText(dopText); } else { tvDOP.setText("📈 DOP: PDOP=-- HDOP=-- VDOP=--"); } } // Время фикса if (tvFixTime != null) { if (currentVessel.getFixTime() > 0) { java.util.Date fixDate = new java.util.Date(currentVessel.getFixTime()); String fixTimeText = String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(fixDate)); tvFixTime.setText(fixTimeText); } else { tvFixTime.setText("🕐 Время фикса: --"); } } // Качество фикса if (tvFixQuality != null) { if (currentVessel.getFixQuality() != null) { String fixQualityText = String.format("🔒 Качество фикса: %s", currentVessel.getFixQuality()); tvFixQuality.setText(fixQualityText); } else { tvFixQuality.setText("🔒 Качество фикса: --"); } } }); } /** * Показывает BottomSheet с информацией об AIS судне */ private void showAISVesselBottomSheet(AISVessel vessel) { if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing()) { currentAISVessel = vessel; updateAISBottomSheetUI(vessel); aisVesselBottomSheet.show(); startTimeUpdate(); // Запускаем автоматическое обновление BottomSheet startBottomSheetAutoUpdate(); } } /** * Запускает обновление времени */ private void startTimeUpdate() { if (timeUpdateHandler != null && timeUpdateRunnable != null) { timeUpdateHandler.postDelayed(timeUpdateRunnable, 1000); } } /** * Останавливает обновление времени */ private void stopTimeUpdate() { if (timeUpdateHandler != null && timeUpdateRunnable != null) { timeUpdateHandler.removeCallbacks(timeUpdateRunnable); } currentAISVessel = null; } /** * Запускает автоматическое обновление BottomSheet */ private void startBottomSheetAutoUpdate() { if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { // Останавливаем предыдущее обновление, если оно запущено bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); // Запускаем новое обновление bottomSheetUpdateHandler.postDelayed(bottomSheetUpdateRunnable, BOTTOM_SHEET_UPDATE_INTERVAL); Log.i(TAG, "Автоматическое обновление BottomSheet запущено"); } } /** * Останавливает автоматическое обновление BottomSheet */ private void stopBottomSheetAutoUpdate() { if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); Log.i(TAG, "Автоматическое обновление BottomSheet остановлено"); } } /** * Обновляет только время назад для AIS судна */ private void updateAISTimeAgo() { if (aisBottomSheetView == null || currentAISVessel == null) return; TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); if (tvTimeAgo != null && currentAISVessel.getLastUpdate() != null) { long secondsAgo = java.time.Duration.between(currentAISVessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); String timeAgoText = formatTimeAgo(secondsAgo); tvTimeAgo.setText("⏱️ Время назад: " + timeAgoText); } } /** * Обновляет UI AIS BottomSheet с актуальными данными */ private void updateAISBottomSheetUI(AISVessel vessel) { if (aisBottomSheetView == null || vessel == null) return; // Обновляем текущее судно, если это то же самое судно if (currentAISVessel != null && currentAISVessel.getMmsi() != null && currentAISVessel.getMmsi().equals(vessel.getMmsi())) { currentAISVessel = vessel; } // Убеждаемся, что обновление происходит в главном потоке runOnUiThread(() -> { if (aisBottomSheetView == null || vessel == null) return; // Обновляем все поля в AIS BottomSheet TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title); TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi); TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign); TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo); TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type); TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position); TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course); TextView tvRot = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_rot); TextView tvHeading = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_heading); TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed); TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions); TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft); TextView tvDestination = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_destination); TextView tvEta = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_eta); TextView tvNavStatus = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_nav_status); TextView tvClass = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_class); TextView tvSignal = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_signal); TextView tvDistance = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_distance); TextView tvBearing = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_bearing); TextView tvLastUpdate = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_last_update); TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); // Заголовок if (tvTitle != null) { String name = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty() ? vessel.getVesselName() : "AIS СУДНО"; String flag = getFlagEmojiForMMSI(vessel.getMmsi()); String title = (flag != null ? flag + " " : "") + "🚢 " + name; tvTitle.setText(title); } // MMSI if (tvMmsi != null) { tvMmsi.setText("🆔 MMSI: " + (vessel.getMmsi() != null ? vessel.getMmsi() : "--")); } // Название судна // Позывной if (tvCallsign != null) { tvCallsign.setText("📻 Позывной: " + (vessel.getCallSign() != null ? vessel.getCallSign() : "--")); } // IMO if (tvImo != null) { tvImo.setText("🏷️ IMO: " + (vessel.getImo() > 0 ? String.valueOf(vessel.getImo()) : "--")); } // Тип судна if (tvType != null) { tvType.setText("🚢 Тип: " + (vessel.getVesselType() != null ? vessel.getVesselType() : "--")); } // Координаты if (tvPosition != null) { if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { String positionText = String.format("📍 Координаты: %.6f, %.6f", vessel.getLatitude(), vessel.getLongitude()); tvPosition.setText(positionText); } else { tvPosition.setText("📍 Координаты: --"); } } // Курс (COG) if (tvCourse != null) { if (vessel.getCourse() > 0) { String courseText = String.format("🧭 COG: %.1f°", vessel.getCourse()); tvCourse.setText(courseText); } else { tvCourse.setText("🧭 COG: --°"); } } // Скорость поворота (ROT) if (tvRot != null) { double rot = vessel.getRateOfTurn(); if (rot != 0) { String rotText = String.format("🔄 ROT: %.1f°/мин", rot); tvRot.setText(rotText); } else { tvRot.setText("🔄 ROT: --°/мин"); } } // Направление (HDG) if (tvHeading != null) { if (vessel.getHeading() > 0) { String headingText = String.format("🧭 HDG: %.1f°", vessel.getHeading()); tvHeading.setText(headingText); } else { tvHeading.setText("🧭 HDG: --°"); } } // Скорость if (tvSpeed != null) { if (vessel.getSpeed() > 0) { String speedText = String.format("⚡ Скорость: %.1f узлов", vessel.getSpeed()); tvSpeed.setText(speedText); } else { tvSpeed.setText("⚡ Скорость: -- узлов"); } } // Размеры if (tvDimensions != null) { if (vessel.getLength() > 0 && vessel.getWidth() > 0) { String dimensionsText = String.format("📏 Размеры: %.1f x %.1f м", vessel.getLength(), vessel.getWidth()); tvDimensions.setText(dimensionsText); } else { tvDimensions.setText("📏 Размеры: --"); } } // Осадка if (tvDraft != null) { if (vessel.getDraft() > 0) { String draftText = String.format("🌊 Осадка: %.1f м", vessel.getDraft()); tvDraft.setText(draftText); } else { tvDraft.setText("🌊 Осадка: -- м"); } } // Пункт назначения if (tvDestination != null) { tvDestination.setText("🎯 Назначение: " + (vessel.getDestination() != null ? vessel.getDestination() : "--")); } // ETA if (tvEta != null) { if (vessel.getEta() != null) { String etaText = String.format("⏰ ETA: %s", vessel.getEta().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); tvEta.setText(etaText); } else { tvEta.setText("⏰ ETA: --"); } } // Навигационный статус if (tvNavStatus != null) { tvNavStatus.setText("🚦 Статус: " + (vessel.getNavigationalStatus() != null ? vessel.getNavigationalStatus() : "--")); } // Класс судна if (tvClass != null) { tvClass.setText("📋 Класс: " + (vessel.getVesselClass() != null ? vessel.getVesselClass() : "--")); } // Сила сигнала if (tvSignal != null) { if (vessel.getSignalStrength() > 0) { String signalText = String.format("📶 Сигнал: %d", vessel.getSignalStrength()); tvSignal.setText(signalText); } else { // Показываем качество позиции по AIS Accuracy биту String qualityText = vessel.isPositionAccuracy() ? "📶 Точность: высокая" : "📶 Точность: низкая"; tvSignal.setText(qualityText); } } // Последнее обновление if (tvLastUpdate != null) { if (vessel.getLastUpdate() != null) { String lastUpdateText = String.format("🕐 Обновлено: %s", vessel.getLastUpdate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))); tvLastUpdate.setText(lastUpdateText); } else { tvLastUpdate.setText("🕐 Обновлено: --"); } } // Расстояние до судна if (tvDistance != null) { Vessel ourVessel = appController.getOwnVessel(); if (ourVessel != null && ourVessel.getLatitude() != 0 && ourVessel.getLongitude() != 0 && vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { double distance = com.grigowashere.aismap.utils.NavigationUtils.calculateDistance( ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude() ); String distanceText = "📏 Расстояние: " + com.grigowashere.aismap.utils.NavigationUtils.formatDistance(distance); tvDistance.setText(distanceText); } else { tvDistance.setText("📏 Расстояние: --"); } } // Пеленг (азимут) до судна if (tvBearing != null) { Vessel ourVessel = appController.getOwnVessel(); if (ourVessel != null && ourVessel.getLatitude() != 0 && ourVessel.getLongitude() != 0 && vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { double bearing = com.grigowashere.aismap.utils.NavigationUtils.calculateBearing( ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude() ); double relativeBearing = com.grigowashere.aismap.utils.NavigationUtils.calculateRelativeBearing( ourVessel.getCourse(), bearing ); String bearingText = "🧭 Пеленг: " + com.grigowashere.aismap.utils.NavigationUtils.formatRelativeBearing(relativeBearing); tvBearing.setText(bearingText); } else { tvBearing.setText("🧭 Пеленг: --"); } } // Время назад if (tvTimeAgo != null) { if (vessel.getLastUpdate() != null) { long secondsAgo = java.time.Duration.between(vessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); String timeAgoText = formatTimeAgo(secondsAgo); tvTimeAgo.setText("⏱️ Время назад: " + timeAgoText); } else { tvTimeAgo.setText("⏱️ Время назад: --"); } } }); } /** * Форматирует время назад в читаемый вид */ private String formatTimeAgo(long seconds) { if (seconds < 60) { return seconds + " сек"; } else if (seconds < 3600) { long minutes = seconds / 60; return minutes + " мин"; } else if (seconds < 86400) { long hours = seconds / 3600; return hours + " ч"; } else { long days = seconds / 86400; return days + " дн"; } } /** * Возвращает флаг-эмодзи по MMSI через MID->ISO2. */ private String getFlagEmojiForMMSI(String mmsi) { try { if (mmsi == null || mmsi.length() < 3) return null; String mid = mmsi.substring(0, 3); String iso2 = MIDToCountry.MID_TO_COUNTRY.get(mid); if (iso2 == null || iso2.length() != 2) return null; char a = Character.toUpperCase(iso2.charAt(0)); char b = Character.toUpperCase(iso2.charAt(1)); int base = 0x1F1E6; int cp1 = base + (a - 'A'); int cp2 = base + (b - 'A'); return new String(Character.toChars(cp1)) + new String(Character.toChars(cp2)); } catch (Exception ignored) { return null; } } /** * Восстанавливает обработчики кликов для маркеров */ private void restoreMarkerClickListeners() { Log.i(TAG, "Восстанавливаем обработчики кликов для маркеров"); if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) { com.grigowashere.aismap.maps.YandexMapImpl yandexMap = (com.grigowashere.aismap.maps.YandexMapImpl) mapInterface; yandexMap.refreshMarkerClickListeners(); Log.i(TAG, "Обработчики кликов восстановлены"); } } /** * Тестирует работу кликов по маркерам */ private void testMarkerClicks() { Log.i(TAG, "Тестируем работу кликов по маркерам"); Toast.makeText(this, "Тестируем клики по маркерам", Toast.LENGTH_SHORT).show(); // Восстанавливаем обработчики кликов restoreMarkerClickListeners(); // Проверяем, что маркеры существуют Vessel ownVessel = appController.getOwnVessel(); if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) { Log.i(TAG, "Наше судно найдено, координаты: " + ownVessel.getLatitude() + ", " + ownVessel.getLongitude()); Toast.makeText(this, "Наше судно найдено, попробуйте кликнуть по маркеру", Toast.LENGTH_LONG).show(); } else { Log.w(TAG, "Наше судно не найдено или координаты равны 0"); Toast.makeText(this, "Наше судно не найдено", Toast.LENGTH_SHORT).show(); } // Проверяем AIS суда List aisVessels = appController.getAISVessels(); if (!aisVessels.isEmpty()) { Log.i(TAG, "Найдено AIS судов: " + aisVessels.size()); Toast.makeText(this, "Найдено AIS судов: " + aisVessels.size(), Toast.LENGTH_SHORT).show(); } else { Log.w(TAG, "AIS суда не найдены"); Toast.makeText(this, "AIS суда не найдены", Toast.LENGTH_SHORT).show(); } } /** * Инициализирует Яндекс.Карты */ private void initializeYandexMaps() { if (!isYandexMapsInitialized) { try { // Инициализация Яндекс.Карт com.yandex.mapkit.MapKitFactory.setApiKey("9ae1917c-2049-4927-9d1e-29dd0d3e8ebc"); com.yandex.mapkit.MapKitFactory.initialize(this); isYandexMapsInitialized = true; // Устанавливаем флаг в MapController MapController.setYandexMapsInitialized(true); Log.i(TAG, "Яндекс.Карты успешно инициализированы"); } catch (Exception e) { Log.e(TAG, "Ошибка инициализации Яндекс.Карт: " + e.getMessage(), e); } } } /** * Применяет настройки к контроллерам */ private void applySettings() { if (settingsManager == null || appController == null) { Log.w(TAG, "SettingsManager или AppController не инициализированы"); return; } try { // Применяем UDP настройки int udpPort = settingsManager.getUDPPort(); boolean udpEnabled = settingsManager.isUDPEnabled(); appController.setUDPPort(udpPort); appController.setUDPEnabled(udpEnabled); // Применяем NMEA настройки boolean androidNMEAEnabled = settingsManager.isAndroidNMEAEnabled(); boolean udpNMEAEnabled = settingsManager.isUDPNMEAEnabled(); appController.setAndroidNMEAEnabled(androidNMEAEnabled); appController.setUDPNMEAEnabled(udpNMEAEnabled); // Применяем режим данных String dataMode = settingsManager.getDataMode(); appController.setDataMode(dataMode); Log.i(TAG, "Настройки применены: " + settingsManager.getSettingsSummary()); } catch (Exception e) { Log.e(TAG, "Ошибка при применении настроек: " + e.getMessage(), e); Toast.makeText(this, "Ошибка при применении настроек", Toast.LENGTH_SHORT).show(); } } /** * Запускает форграунд сервис */ private void startForegroundService() { try { // Проверяем разрешения для Android 13+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { if (androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { android.util.Log.w(TAG, "Запрашиваем разрешение на уведомления для Android 13+"); androidx.core.app.ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, NOTIFICATION_PERMISSION_REQUEST_CODE); return; // Ждем разрешения } } android.content.Intent svc = new android.content.Intent(this, com.grigowashere.aismap.services.AISForegroundService.class); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { startForegroundService(svc); android.util.Log.i(TAG, "Форграунд сервис запущен через startForegroundService()"); } else { startService(svc); android.util.Log.i(TAG, "Форграунд сервис запущен через startService()"); } } catch (Exception e) { android.util.Log.e(TAG, "Не удалось запустить форграунд сервис: " + e.getMessage(), e); } } /** * Останавливает форграунд сервис */ private void stopForegroundService() { try { android.content.Intent svc = new android.content.Intent(this, com.grigowashere.aismap.services.AISForegroundService.class); stopService(svc); android.util.Log.i(TAG, "Форграунд сервис остановлен"); } catch (Exception e) { android.util.Log.e(TAG, "Ошибка при остановке форграунд сервиса: " + e.getMessage(), e); } } /** * Тестирует работу форграунд сервиса */ private void testForegroundService() { android.util.Log.i(TAG, "=== ТЕСТ ФОРГРАУНД СЕРВИСА ==="); // Проверяем разрешения на уведомления boolean hasNotificationPermission = true; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { hasNotificationPermission = androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == android.content.pm.PackageManager.PERMISSION_GRANTED; } android.util.Log.i(TAG, "Разрешение на уведомления: " + hasNotificationPermission); // Проверяем статус сервиса boolean isServiceRunning = isServiceRunning(); android.util.Log.i(TAG, "Сервис запущен: " + isServiceRunning); if (isServiceRunning) { android.util.Log.i(TAG, "Останавливаем сервис..."); stopForegroundService(); Toast.makeText(this, "Сервис остановлен", Toast.LENGTH_SHORT).show(); } else { android.util.Log.i(TAG, "Запускаем сервис..."); startForegroundService(); Toast.makeText(this, "Сервис запущен", Toast.LENGTH_SHORT).show(); } android.util.Log.i(TAG, "=== КОНЕЦ ТЕСТА ==="); } /** * Проверяет, запущен ли сервис */ private boolean isServiceRunning() { android.app.ActivityManager manager = (android.app.ActivityManager) getSystemService(android.content.Context.ACTIVITY_SERVICE); for (android.app.ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (com.grigowashere.aismap.services.AISForegroundService.class.getName().equals(service.service.getClassName())) { return true; } } return false; } /** * Перезапускает сервисы с новыми настройками */ private void restartServices() { if (appController == null) { Log.w(TAG, "AppController не инициализирован"); return; } try { Log.i(TAG, "Перезапускаем сервисы..."); // Останавливаем все слушатели appController.stopAllListeners(); // Применяем новые настройки applySettings(); // Перезапускаем UDP слушатель с новым портом, если нужно if (settingsManager.shouldRestartUDP(appController.getUDPPort(), appController.isUDPEnabled())) { appController.restartUDPListener(); } // Запускаем слушатели с новыми настройками appController.startAllListeners(); Log.i(TAG, "Сервисы успешно перезапущены"); Log.i(TAG, "Статус настроек: " + appController.getSettingsStatus()); } catch (Exception e) { Log.e(TAG, "Ошибка при перезапуске сервисов: " + e.getMessage(), e); Toast.makeText(this, "Ошибка при перезапуске сервисов", Toast.LENGTH_SHORT).show(); } } /** * Инициализирует курсор согласно настройкам */ private void initializeCursor() { if (mapInterface == null || settingsManager == null) return; boolean cursorEnabled = settingsManager.isCursorEnabled(); if (cursorEnabled) { mapInterface.showCursor(); // Обновляем координаты курсора с центра карты mapInterface.updateCursorFromMapCenter(); } else { mapInterface.hideCursor(); } Log.i(TAG, "Курсор инициализирован: " + (cursorEnabled ? "включен" : "выключен")); } /** * Применяет настройки курсора */ private void applyCursorSettings(boolean cursorEnabled) { if (mapInterface == null) return; if (cursorEnabled) { mapInterface.showCursor(); // Обновляем координаты курсора с центра карты mapInterface.updateCursorFromMapCenter(); } else { mapInterface.hideCursor(); } Log.i(TAG, "Настройки курсора применены: " + (cursorEnabled ? "включен" : "выключен")); } }