generated from Grigo/AndroidTemplate
b5aee265bc
Архитектурные улучшения: - Внедрен UIRenderingCoordinator с централизованным throttling - Решены проблемы зависания UI через батчинг операций карты - Добавлен VesselPathController для отслеживания маршрутов - Реализован MapLibreMapImpl как альтернатива Яндекс.Картам Визуализация AIS: - Добавлены векторные иконки для всех типов судов - Разделение Class A/B судов с соответствующими иконками - Иконки навигационных статусов (anchor, moored, engine, sail) - Улучшенный CursorOverlay с информацией о судах Производительность: - Throttling UI обновлений (vessel: 500ms, AIS: 1s, paths: 2s) - Устранение утечек Handler объектов - Оптимизация GeoJSON операций в MapLibre
2135 lines
96 KiB
Java
2135 lines
96 KiB
Java
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<AISVessel> 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<Thread, StackTraceElement[]> 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<Thread, StackTraceElement[]> 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<AISVessel> 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<AISVessel> 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 ? "включен" : "выключен"));
|
|
}
|
|
|
|
} |