Files
AndroidAisMap/app/src/main/java/com/grigowashere/aismap/MainActivity.java
T
Grigo b5aee265bc feat: новая архитектура UI и расширенная визуализация AIS
Архитектурные улучшения:
- Внедрен 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
2025-10-02 09:15:33 +03:00

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 ? "включен" : "выключен"));
}
}