# Hướng dẫn tích hợp Flutter SDK


Source: https://docs.fleetwork.vn/sdk/tracking-sdk-flutter

> Hướng dẫn tích hợp GPS Tracking cho ứng dụng Flutter mobile (iOS & Android).

**Package:** [vietmap_tracking_plugin](https://pub.dev/packages/vietmap_tracking_plugin)

Plugin xây dựng trên native platform channel (MethodChannel + EventChannel), không dùng thư viện location bên thứ ba. Hỗ trợ tracking nền thời gian thực, cảnh báo tốc độ, cache offline tự đồng bộ, phát hiện GPS giả và tối ưu pin.

> Liên hệ Vietmap để lấy API key trước khi tích hợp: [maps.info@vietmap.vn](mailto:maps.info@vietmap.vn)

---

## Tính năng

| Tính năng                 | Mô tả                                                                                     |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| **Background GPS**        | Foreground service (Android) / background location mode (iOS).                             |
| **Xác thực API key**      | `initializeTracking` xác thực phía server — throw `INVALID_API_KEY` nếu key không hợp lệ.  |
| **Metadata**              | `setMetadata` đính kèm key-value tuỳ ý vào mỗi bản ghi GPS.                                |
| **Package Codes**         | `setPackages` gắn danh sách mã kiện hàng vào mỗi bản ghi GPS.                              |
| **Cảnh báo tốc độ**       | Giám sát tốc độ thời gian thực qua Alert API.                                              |
| **Offline Cache**         | Cache SQLite, tự upload khi có mạng trở lại.                                               |
| **Speed Fallback**        | Tự tính tốc độ khi hệ điều hành không trả về (Xiaomi/MIUI); gắn cờ `speedSource`.          |
| **Fake GPS Detection**    | Phát hiện ở native với 4 policy; SDK tự bắn notification kể cả khi app bị kill.            |
| **Smart Battery**         | Opt-in — điều chỉnh tần suất tracking theo mức pin và chuyển động.                         |
| **Tracking Presets**      | Navigation / Fitness / General / BatterySaver — bản theo thời gian và theo khoảng cách.    |
| **Tracking Interrupted**  | Phát hiện khi tracking nền bị hệ điều hành dừng và khi khôi phục.                          |
| **Location Utilities**    | Tính khoảng cách Haversine, đổi đơn vị tốc độ, kiểm tra geofence.                          |

---

## Yêu cầu hệ thống

|         | Minimum               |
| ------- | --------------------- |
| Flutter | 3.3.0+                |
| Dart    | 3.8.0+                |
| Android | API 21+ (Android 5.0) |
| iOS     | 15.0+                 |

**Native SDK đi kèm**

| Nền tảng | SDK                                                    | Phiên bản |
| -------- | ------------------------------------------------------ | --------- |
| iOS      | VietmapTrackingSDK (CocoaPods)                         | 1.5.1     |
| Android  | com.github.vietmap-company:vietmap-tracking-sdk-android | 1.5.2     |

---

## 1. Cài đặt

### pubspec.yaml

```yaml
dependencies:
  vietmap_tracking_plugin: ^1.1.3
```

```bash
flutter pub get
```

---

## 2. Cấu hình Android

### 2.1. Quyền — `android/app/src/main/AndroidManifest.xml`

```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Bắt buộc để hiện notification trên Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Bắt buộc để đặt lại notification sau khi khởi động lại máy -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```

> **Android 10+ (API 29+):** `ACCESS_BACKGROUND_LOCATION` phải được xin **sau khi** quyền foreground đã được cấp. Plugin tự xử lý luồng 2 bước này.

### 2.2. Receiver cho notification — trong khối `<application>`

Bắt buộc để `flutter_local_notifications` xin được quyền runtime:

```xml
<receiver android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
        <action android:name="android.intent.action.QUICKBOOT_POWERON" />
        <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
    </intent-filter>
</receiver>
<receiver android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
```

### 2.3. Maven repository — `android/build.gradle`

```groovy
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
```

---

## 3. Cấu hình iOS

### 3.1. Mô tả quyền vị trí — `ios/Runner/Info.plist`

```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng cần GPS để theo dõi vị trí khi đang chạy.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Ứng dụng cần GPS để tiếp tục theo dõi khi chạy nền.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Ứng dụng cần GPS khi chạy nền.</string>
```

### 3.2. Background Modes — `ios/Runner/Info.plist`

```xml
<key>UIBackgroundModes</key>
<array>
    <string>location</string>
    <string>background-processing</string>
    <string>background-fetch</string>
</array>
```

### 3.3. Background Task Identifiers (iOS 15+)

```xml
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.vietmaptrackingsdk.location-sync</string>
    <string>com.vietmaptrackingsdk.background-location</string>
</array>
```

### 3.4. Notification khi app đang mở — `ios/Runner/AppDelegate.swift`

Mặc định iOS ẩn banner notification khi app đang ở foreground. Cần 2 thay đổi:

1. Gán `UNUserNotificationCenter.current().delegate = self` **trước** `GeneratedPluginRegistrant.register(with: self)`.
2. Override `userNotificationCenter(_:willPresent:)` và gọi thẳng `completionHandler([.banner, .sound, .badge])` — **không gọi `super`**.

### 3.5. CocoaPods

```ruby
# ios/Podfile
platform :ios, '15.0'
```

```bash
cd ios && pod install
```

> **Lần cài đầu tiên:** chạy `pod install --repo-update` để `VietmapTrackingSDK` lấy đúng spec mới nhất.

---

## 4. Khởi tạo SDK

```dart
import 'package:flutter/services.dart';
import 'package:vietmap_tracking_plugin/vietmap_tracking_plugin.dart';

final controller = VietmapTrackingController.instance;

Future<void> initSdk() async {
  try {
    // Xác thực API key phía server — throw INVALID_API_KEY nếu bị từ chối
    await controller.initializeTracking(
      'YOUR_API_KEY',
      baseURL: 'https://live.fleetwork.vn/api/v1', // tuỳ chọn
    );
    // Đăng ký lifecycle observer để SDK tự chuyển background/foreground
    controller.registerLifecycleObserver();
  } on PlatformException catch (e) {
    if (e.code == 'INVALID_API_KEY') {
      print('API key không hợp lệ: ${e.message}');
    }
  }
}
```

> Gọi `initializeTracking()` **trước** tất cả method khác. `baseURL` là tuỳ chọn — SDK dùng URL production mặc định.

---

## 5. Quyền truy cập

```dart
// Bước 1 — quyền vị trí foreground
final result = await controller.requestLocationPermissions();
if (!result.granted) return;

// Bước 2 — quyền background (Android 10+ hiện popup riêng)
await controller.requestAlwaysLocationPermissions();

// Bước 3 — quyền notification (Android 13+ / iOS)
// Android
await Permission.notification.request();
// iOS
await FlutterLocalNotificationsPlugin()
    .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
    ?.requestPermissions(alert: true, sound: true, badge: false);
```

**Kiểm tra trạng thái quyền (không hiện popup):**

```dart
final result = await controller.hasLocationPermissions();
if (!result.granted) {
  // Dẫn user đến màn hình cấp quyền
}
```

---

## 6. Bắt đầu / Dừng theo dõi

> **`userId` là bắt buộc** — bản ghi GPS được gắn theo giá trị này ở phía server. Thiếu `userId` thì bản ghi không xác định được chủ sở hữu.

```dart
await controller.startTracking(
  LocationTrackingConfig(
    userId: 'user-123',            // bắt buộc
    intervalMs: 30000,             // 30 giây / lần
    accuracy: LocationAccuracy.high,
    backgroundMode: true,
    notificationTitle: 'Đang theo dõi hành trình',
    notificationMessage: 'Chạm để mở ứng dụng',
  ),
);

// Hoặc dùng preset dựng sẵn:
await controller.startTracking(
  TrackingPresets.general().copyWith(userId: 'user-123'),
);

// Hoặc để native SDK tự quyết định tần suất (timer 10 giây / sàn 25 mét):
await controller.startTracking(
  LocationTrackingConfig.sdkDefault(userId: 'user-123'),
);

// Dừng tracking
await controller.stopTracking();
controller.unregisterLifecycleObserver();
```

> Tracking giữ nguyên tần suất bạn truyền vào. [Smart Battery](#9-smart-battery-optimization) là **opt-in** — truyền `enableSmartBattery: true` nếu muốn SDK tự điều chỉnh interval theo mức pin và chuyển động.

---

## 7. Chế độ tracking & Presets

`LocationTrackingConfig` hỗ trợ 2 chiến lược **loại trừ lẫn nhau**. Chỉ truyền **một** trong `intervalMs` hoặc `distanceFilter`, cái còn lại để `null`.

| Chế độ                       | `intervalMs` | `distanceFilter` | Hành vi                                                                                          |
| ---------------------------- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------ |
| **Interval (theo thời gian)** | `> 0`        | `null`           | Cập nhật mỗi N mili-giây, bất kể có di chuyển hay không.                                          |
| **Distance (theo khoảng cách)** | `null`     | `> 0`            | Chỉ cập nhật sau khi thiết bị di chuyển M mét.                                                    |
| **SDK default**              | `null`       | `null`           | Dùng tần suất mặc định của native SDK — timer 10 giây, sàn khoảng cách 25 mét. Dùng `LocationTrackingConfig.sdkDefault()`. |

> SDK nâng `distanceFilter` lên **sàn 25 mét**. Nếu truyền cả hai giá trị thì timer thắng và distance bị bỏ qua (logic giống nhau trên Android và iOS).

### Preset dựng sẵn

**Theo thời gian:**

```dart
TrackingPresets.navigation()    //  5 giây  — theo dõi xe thời gian thực / dẫn đường
TrackingPresets.fitness()       // 10 giây  — hoạt động ngoài trời
TrackingPresets.general()       // 30 giây  — cân bằng cho đội xe (mặc định)
TrackingPresets.batterySaver()  //  5 phút  — xe đỗ hoặc pin yếu
```

**Theo khoảng cách:**

```dart
TrackingPresets.navigationDistance()    // mỗi  25 m — sàn khoảng cách của SDK
TrackingPresets.fitnessDistance()       // mỗi  50 m
TrackingPresets.generalDistance()       // mỗi  70 m
TrackingPresets.batterySaverDistance()  // mỗi 120 m
```

Luôn gắn `userId` qua `copyWith`:

```dart
await controller.startTracking(
  TrackingPresets.general().copyWith(userId: 'user-123'),
);
```

---

## 8. Kiểm tra trạng thái & cập nhật cấu hình

```dart
final isActive = await controller.isTrackingActive(); // bool

final status = await controller.getTrackingStatus();
print(status.isTracking);          // bool
print(status.trackingDuration);    // int (ms kể từ startTracking)
print(status.lastLocationUpdate);  // int? (unix ms)

// Đổi config mà không cần restart tracking
await controller.updateTrackingConfig(
  LocationTrackingConfig(
    userId: 'user-123',
    intervalMs: 60000,
    accuracy: LocationAccuracy.medium,
    backgroundMode: true,
  ),
);
```

> **Lưu ý iOS với `updateTrackingConfig`:** iOS thực hiện `stop → start` ở bên trong. Hệ quả: có khoảng trống vị trí ngắn, `trackingDuration` bị reset, và stream trạng thái bắn `false → true`.

---

## 9. Smart Battery Optimization

Smart Battery là **opt-in**. Truyền `enableSmartBattery: true` vào `startTracking()`; `stopTracking()` sẽ tắt lại.

```dart
// Đặt profile ưu tiên khi xe đang di chuyển (mặc định: general)
controller.setSmartBatteryPreferredProfile(SmartBatteryProfile.navigation);

await controller.startTracking(config, enableSmartBattery: true);

// Feed GPS vào Smart Battery để phát hiện vào cua và đứng yên
controller.onLocationUpdate.listen((LocationData loc) {
  controller.feedLocationToSmartBattery(loc.speed, heading: loc.bearing);
});

// Lắng nghe khi profile thay đổi
controller.onSmartBatteryProfileChanged.listen((profile) {
  print('Battery profile: $profile');
});
```

> `feedLocationToSmartBattery` cấp tốc độ và hướng cho bộ điều phối profile — nhờ đó SDK phát hiện được xe đứng yên (≥ 60 giây) và vào cua (heading lệch > 15°) để tự chuyển profile.

Có thể bật/tắt giữa phiên:

```dart
await controller.enableSmartBatteryOptimization();   // áp preset của profile hiện tại
controller.disableSmartBatteryOptimization();        // chỉ teardown phía Dart
await controller.updateTrackingConfig(myConfig);     // …nên khôi phục lại config sau khi tắt
```

| Profile        | Interval | Khi nào dùng                    |
| -------------- | -------- | ------------------------------- |
| `navigation`   | 5 giây   | Dẫn đường, cần độ chính xác cao |
| `general`      | 30 giây  | Theo dõi đội xe tiêu chuẩn      |
| `batterySaver` | 5 phút   | Pin yếu / xe đang đỗ            |

> **Vì sao là opt-in:** ngay khi bật, SDK áp preset native của profile đang active, ghi đè interval mà `startTracking()` vừa nhận — một config yêu cầu SDK default 10 giây sẽ bị đẩy về 30 giây của `general` chỉ vài khoảnh khắc sau khi start. Việc **giãn upload** khi xe đứng yên (1 heartbeat mỗi 5 phút) là cơ chế native riêng, luôn bật và không phụ thuộc flag này.

SDK tự chuyển profile theo điều kiện:

- Pin < 15% và không sạc → `batterySaver`
- Xe đứng yên ≥ 60 giây → `general`
- Xe vào cua (heading lệch > 15°) → `navigation`
- Còn lại → `preferredMovingProfile` (mặc định: `general`)

### Config tuỳ chỉnh kết hợp Smart Battery

Phần này **chỉ áp dụng khi Smart Battery được bật** — không bật thì config giữ nguyên như bạn truyền vào.

Khi đã bật, Smart Battery ghi đè interval native ở **mọi lần chuyển profile** (pin yếu, đứng yên, vào cua). Với preset chuẩn thì đây là hành vi mong muốn. Với `LocationTrackingConfig` tuỳ chỉnh mang giá trị không thuộc preset, hãy đăng ký callback `customGeneralConfigOverride` **trước khi** gọi `startTracking()` để config của bạn được áp lại thay vì preset native.

```dart
// Đăng ký trước startTracking — yêu cầu Smart Battery áp lại config của bạn
// thay vì chuyển sang preset native ở mỗi lần đổi profile.
SmartBatteryManager.instance.customGeneralConfigOverride = () async {
  await controller.updateTrackingConfig(myCustomConfig);
};

await controller.startTracking(myCustomConfig, enableSmartBattery: true);
```

Xoá override khi dừng để nó không tồn tại sang phiên sau:

```dart
await controller.stopTracking();
SmartBatteryManager.instance.customGeneralConfigOverride = null;
```

> **Lưu ý:** Callback được gọi ở mọi lần chuyển profile — kể cả `batterySaver` và `navigation` — nên Smart Battery vẫn phát hiện và báo cáo thay đổi profile qua `onSmartBatteryProfileChanged`, nhưng interval tracking thực tế luôn theo config của bạn.

---

## 10. Package Codes

`setPackages` gắn danh sách mã kiện hàng vào mỗi bản ghi GPS dưới dạng trường **top-level** `packages` — nằm cạnh `metadata`, không nằm trong `metadata`:

```json
{
  "time": 1777876800000,
  "lat": 10.762219,
  "lng": 106.656956,
  "packages": ["#10001", "#10002"],
  "metadata": {}
}
```

```dart
await controller.setPackages(['#10001', '#10002']);

// Danh sách rỗng = xoá trường — payload bỏ hẳn field, không gửi []
await controller.setPackages([]);
```

Trường này là tuỳ chọn, có thể set trước hoặc trong khi tracking. Danh sách được chốt **theo từng GPS point tại thời điểm ghi nhận**, nên một point cache offline vẫn giữ mã kiện lúc nó được capture, thay vì lấy giá trị đang set khi upload.

---

## 11. Offline Cache & Sync

SDK **tự động** lưu local khi mất mạng và gửi lại khi có kết nối.

```dart
// Tắt auto upload để tự kiểm soát
await controller.setAutoUpload(false);

final pending   = await controller.getCachedLocationsCount(); // số điểm chờ gửi
final sizeBytes = await controller.getDatabaseSizeBytes();    // kích thước cache

// Cấu hình giới hạn cache
await controller.configureCacheLimits(
  maxRecords: 5000,
  maxDbSizeBytes: 50 * 1024 * 1024, // 50 MB
  batchSize: 50,
);

// Gửi cache ngay / xoá toàn bộ cache (không hoàn tác)
await controller.uploadCachedLocationsManually();
await controller.clearCachedLocations();
```

---

## 12. Fake GPS Detection

Native SDK tự phát hiện vị trí giả lập. Kể cả khi app bị kill, SDK vẫn bắn notification qua kênh `VietmapTracking` trên Android (không phụ thuộc `flutter_local_notifications`).

### 12.1. Bật phát hiện

Fake GPS detection **mặc định tắt** (`allowMockLocation: true`). Để bật:

```dart
await controller.startTracking(
  TrackingPresets.general().copyWith(
    userId: 'user-123',
    allowMockLocation: false, // bật kiểm tra fake GPS ở native
  ),
);
await controller.setFakeGpsPolicy(FakeGpsPolicy.warn);
```

### 12.2. Chọn policy

| Policy         | Hành vi                                                            |
| -------------- | ------------------------------------------------------------------ |
| `skip`         | Chỉ bắn event — không notification, không dừng tracking. **Mặc định.** |
| `warn`         | SDK bắn notification native (debounce 30 giây mỗi cửa sổ).         |
| `stopTracking` | Tự dừng tracking ngay lần phát hiện đầu tiên.                      |
| `logToServer`  | Vẫn upload bản ghi kèm `is_fake=1` + header `X-Fake-GPS: true`.    |

```dart
await controller.setFakeGpsPolicy(FakeGpsPolicy.warn);

controller.onFakeGpsDetected.listen((FakeGpsEvent ev) {
  print('Fake GPS: lat=${ev.lat}, lng=${ev.lng}, reason=${ev.reason}');
});
```

### 12.3. Tuỳ chỉnh nội dung notification

```dart
await controller.setFakeGpsNotificationConfig(
  title: 'Phát hiện GPS giả',
  message: 'Vui lòng tắt ứng dụng giả lập vị trí để dữ liệu được ghi nhận chính xác.',
);
```

> Gọi sau `initializeTracking`. SDK lưu cấu hình này suốt vòng đời tiến trình (kể cả khi app chạy nền hoặc bị kill trên Android).

### 12.4. Cơ chế phát hiện theo nền tảng

| Nền tảng      | Cơ chế phát hiện                             |
| ------------- | -------------------------------------------- |
| iOS 15+       | `CLLocationSourceInformation`                |
| Android 12+   | `Location.isMock()`                          |
| Android 18–30 | `Location.isFromMockProvider()`              |

Trên iOS, icon notification luôn là icon app. Trường `reason` trong `FakeGpsEvent` nhận giá trị `"simulatedBySoftware"` hoặc `"producedByAccessory"`.

---

## 13. Speed Fallback

Một số thiết bị — điển hình là Xiaomi/MIUI với chế độ tiết kiệm pin gắt — trả về GPS fix **không có tốc độ hợp lệ** (hoặc báo `speed = 0` trong khi xe rõ ràng đang chạy). Mặc định SDK phát hiện tình huống này và **tự tính tốc độ** từ khoảng cách Haversine tới fix trước đó chia cho thời gian trôi qua, để giá trị gửi lên server phản ánh chuyển động thật thay vì `0`.

Cơ chế fallback thận trọng và tự hiệu chỉnh:

- Chỉ kích hoạt khi tốc độ từ hệ điều hành bị thiếu hoặc thấp bất hợp lý so với quãng đường thực tế; tốc độ phần cứng hợp lệ luôn được tin dùng.
- Có kiểm chứng theo độ chính xác của fix và khoảng thời gian, đồng thời qua bộ chặn outlier theo cửa sổ trượt để một cú nhảy GPS không tạo ra spike.
- Fix thực sự đứng yên vẫn giữ `0`.

Mỗi bản ghi upload mang nguồn gốc tốc độ trong `metadata.speedSource`:

| `speedSource` | Ý nghĩa                                                                  |
| ------------- | ------------------------------------------------------------------------ |
| `gps`         | Tốc độ phần cứng từ chip (dùng nguyên giá trị).                          |
| `derived`     | Hệ điều hành không trả tốc độ → tính từ vị trí/thời gian.                |
| `corrected`   | Hệ điều hành trả tốc độ ~0 trong khi đang di chuyển → thay bằng giá trị tính. |
| `unknown`     | Không có tốc độ và fallback không kiểm chứng được (gửi `0`).             |

Mặc định bật. Tắt đi (giữ nguyên tốc độ thô từ hệ điều hành) qua `LocationTrackingConfig`:

```dart
await controller.startTracking(
  TrackingPresets.general().copyWith(
    userId: 'user-123',
    enableSpeedFallback: false, // mặc định true
  ),
);
```

---

## 14. Cảnh báo tốc độ

```dart
// Gọi sau initializeTracking
await controller.configureAlertAPI('your-alert-api-key', 'your-alert-api-id');

await controller.turnOnAlert();
await controller.turnOffAlert();
```

---

## 15. Tracking Interrupted

Bắn event khi tracking nền ngừng sinh GPS trong lúc phiên vẫn đang active, và bắn lại khi khôi phục. Hệ điều hành **không tự resume** — dùng event này để nhắc người dùng dừng và bắt đầu tracking lại.

```dart
await controller.setTrackingInterruptedNotificationEnabled(true);
await controller.setTrackingInterruptedNotificationConfig(
  title: 'Theo dõi bị gián đoạn',
  message: 'Vui lòng mở ứng dụng và bật lại theo dõi hành trình.',
);

controller.onTrackingInterrupted.listen((TrackingInterruptedEvent ev) {
  print('${ev.reason} — recovered: ${ev.recovered}');
});
```

`reason` do SDK quản lý, là một trong các hằng `TrackingInterruptedReason`: `locationUnavailable`, `providerDisabled`, `paused`, `authDowngraded`, `authDenied`, `locationServicesOff`, `permissionRevoked`, `staleNoUpdates`.

> Tắt notification **không** làm dừng stream — event vẫn tiếp tục được bắn.

---

## 16. Event Streams

```dart
controller.onLocationUpdate            // Stream<LocationData>
controller.onTrackingStatusChanged     // Stream<TrackingStatus>
controller.onFakeGpsDetected           // Stream<FakeGpsEvent>
controller.onTrackingInterrupted       // Stream<TrackingInterruptedEvent>
controller.onSmartBatteryProfileChanged // Stream<SmartBatteryProfile>
```

```dart
controller.onLocationUpdate.listen((LocationData loc) {
  print('${loc.latitude}, ${loc.longitude} @ ${loc.speed} m/s');
});

controller.onTrackingStatusChanged.listen((TrackingStatus s) {
  print('Tracking: ${s.isTracking}');
});

controller.onFakeGpsDetected.listen((FakeGpsEvent ev) {
  print('Fake GPS tại ${ev.lat}, ${ev.lng}');
});
```

---

## 17. Lịch sử hành trình

```dart
final List<GpsLocation> history = await controller.getTrackingHistory(
  userId: 'user-123',
  fromTimestamp: DateTime(2026, 5, 1).millisecondsSinceEpoch,  // tuỳ chọn
  toTimestamp: DateTime(2026, 5, 12).millisecondsSinceEpoch,   // tuỳ chọn
  pageNumber: 1,
  pageSize: 50,
  sortDescending: true,
);
```

---

## 18. Data Models

### `LocationTrackingConfig`

| Field                 | Kiểu               | Mặc định | Mô tả                                                    |
| --------------------- | ------------------ | -------- | -------------------------------------------------------- |
| **`userId`**          | `String?`          | `null`   | **Bắt buộc.** Bản ghi GPS được gắn theo giá trị này.     |
| `vehicleId`           | `String?`          | `null`   | ID phương tiện (tuỳ chọn).                               |
| `intervalMs`          | `int?`             | `null`   | Chế độ timer: cập nhật mỗi N ms.                         |
| `distanceFilter`      | `double?`          | `null`   | Chế độ khoảng cách: cập nhật sau mỗi M mét.              |
| `accuracy`            | `LocationAccuracy` | `high`   | `high` / `medium` / `low`.                               |
| `backgroundMode`      | `bool`             | `true`   | Tiếp tục tracking khi app ở nền.                         |
| `notificationTitle`   | `String?`          | —        | Android: tiêu đề notification foreground service.        |
| `notificationMessage` | `String?`          | —        | Android: nội dung notification foreground service.       |
| `allowMockLocation`   | `bool`             | `true`   | `false` để bật phát hiện fake GPS ở native.              |
| `enableSpeedFallback` | `bool`             | `true`   | `false` để giữ nguyên tốc độ thô từ hệ điều hành.        |

### `LocationData`

| Field       | Kiểu       | Đơn vị  | Mô tả                       |
| ----------- | ---------- | ------- | --------------------------- |
| `latitude`  | `double`   | độ      | Vĩ độ.                      |
| `longitude` | `double`   | độ      | Kinh độ.                    |
| `altitude`  | `double`   | mét     | Độ cao so với mực nước biển. |
| `accuracy`  | `double`   | mét     | Độ chính xác ngang.         |
| `speed`     | `double`   | **m/s** | Tốc độ tức thời.            |
| `bearing`   | `double`   | 0–360°  | Hướng đầu xe (0 = Bắc).     |
| `timestamp` | `int`      | unix ms | Thời điểm capture.          |
| `dateTime`  | `DateTime` | —       | Getter từ `timestamp`.      |

> **Lưu ý:** `speed` đơn vị là **m/s** (mét/giây). Đổi sang km/h: `speed * 3.6` hoặc `LocationUtils.metersPerSecondToKmh(speed)`.

### `TrackingStatus`

| Field                | Kiểu        | Mô tả                           |
| -------------------- | ----------- | ------------------------------- |
| `isTracking`         | `bool`      | Tracking đang chạy.             |
| `lastLocationUpdate` | `int?`      | Unix ms lần GPS gần nhất.       |
| `trackingDuration`   | `int`       | ms kể từ `startTracking()`.     |
| `lastUpdateTime`     | `DateTime?` | Getter từ `lastLocationUpdate`. |
| `duration`           | `Duration`  | Getter từ `trackingDuration`.   |

### `FakeGpsEvent`

| Field              | Kiểu     | Mô tả                                                            |
| ------------------ | -------- | ---------------------------------------------------------------- |
| `lat` / `lng`      | `double` | Toạ độ bị phát hiện là giả lập.                                  |
| `timestamp`        | `double` | Unix timestamp theo giây.                                        |
| `isFirstDetection` | `bool`   | Lần phát hiện đầu tiên trong cửa sổ debounce 30 giây.            |
| `reason`           | `String` | Chỉ iOS: `"simulatedBySoftware"` \| `"producedByAccessory"`.      |

### `TrackingInterruptedEvent`

| Field                 | Kiểu     | Mô tả                                             |
| --------------------- | -------- | ------------------------------------------------- |
| `reason`              | `String` | Do SDK quản lý — xem `TrackingInterruptedReason`. |
| `recovered`           | `bool`   | `true` khi vị trí đã hoạt động trở lại.           |
| `isInBackground`      | `bool`   | App đang ở nền khi event bắn.                     |
| `secondsSinceLastFix` | `int`    | `-1` nếu không xác định.                          |

### `PermissionResult`

| Field                | Kiểu     | Mô tả                                       |
| -------------------- | -------- | ------------------------------------------- |
| `granted`            | `bool`   | Đủ quyền.                                   |
| `status`             | `String` | `'granted'` / `'denied'` / `'notGranted'`.  |
| `fineLocation`       | `bool`   | Quyền GPS chính xác.                        |
| `coarseLocation`     | `bool`   | Quyền GPS xấp xỉ.                           |
| `backgroundLocation` | `bool`   | Quyền background.                           |

### `SmartBatteryProfile`

```dart
enum SmartBatteryProfile {
  navigation,   // 5 giây — chính xác cao
  general,      // 30 giây — cân bằng mặc định
  batterySaver, // 5 phút — tiết kiệm pin tối đa
}
```

---

## 19. Utility Functions

```dart
// Khoảng cách Haversine (mét)
LocationUtils.calculateDistance(lat1, lng1, lat2, lng2);
LocationUtils.distanceBetween(location1, location2);

// Định dạng toạ độ
LocationUtils.formatCoordinates(21.0285, 105.8542);
// → "21.028500° N, 105.854200° E"

// Đổi đơn vị tốc độ
LocationUtils.metersPerSecondToKmh(25.0);  // → 90.0
LocationUtils.kmhToMetersPerSecond(90.0);  // → 25.0

// Kiểm tra geofence
LocationUtils.isWithinRadius(location, targetLat, targetLng, radiusMetres);
```

---

## 20. Full API Reference

**Khởi tạo**

| Method                                           | Return         | Mô tả                                                                     |
| ------------------------------------------------ | -------------- | ------------------------------------------------------------------------- |
| `initializeTracking(apiKey, {baseURL})`          | `Future<void>` | Xác thực key phía server. Throw `INVALID_API_KEY` nếu thất bại.           |
| `setMetadata(Map)`                               | `Future<void>` | Đính kèm key-value tuỳ ý vào mỗi bản ghi GPS.                             |
| `setPackages(List<String>)`                      | `Future<void>` | Gắn mã kiện hàng vào trường top-level `packages`. List rỗng → bỏ field.   |
| `configureAlertAPI(apiKey, apiID)`               | `Future<void>` | Cấu hình thông tin đăng nhập cảnh báo tốc độ.                             |
| `setFakeGpsNotificationConfig({title, message})` | `Future<void>` | Tuỳ chỉnh notification GPS giả native của SDK.                            |
| `registerLifecycleObserver()`                    | `void`         | Bật chuyển đổi SDK background/foreground tự động.                         |
| `unregisterLifecycleObserver()`                  | `void`         | Gỡ lifecycle observer (gọi khi dừng tracking).                            |

**Điều khiển tracking**

| Method                                                | Return                 | Mô tả                                                                                                                        |
| ----------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `startTracking(config, {enableSmartBattery = false})` | `Future<bool>`         | Bắt đầu GPS tracking. `userId` trong config là bắt buộc. `enableSmartBattery: true` để Smart Battery tự điều chỉnh interval.  |
| `stopTracking()`                                      | `Future<bool>`         | Dừng tracking; SDK có thể flush các bản ghi còn chờ.                                                                          |
| `isTrackingActive()`                                  | `Future<bool>`         | Tracking đang chạy?                                                                                                           |
| `getCurrentLocation()`                                | `Future<LocationData>` | Lấy vị trí hiện tại theo yêu cầu — dùng được cả trước `startTracking()`. Throw nếu không lấy được (thiếu quyền, tắt GPS, timeout). |
| `getTrackingStatus()`                                 | `Future<TrackingStatus>` | Snapshot trạng thái hiện tại.                                                                                               |
| `updateTrackingConfig(config)`                        | `Future<bool>`         | Áp config mới cho phiên đang chạy (xem lưu ý iOS ở mục 8).                                                                    |
| `getTrackingHealthStatus()`                           | `Future<Map>`          | Snapshot chẩn đoán: mạng, cache, SDK flags.                                                                                   |

**Quyền**

| Method                               | Return                     | Mô tả                      |
| ------------------------------------ | -------------------------- | -------------------------- |
| `requestLocationPermissions()`       | `Future<PermissionResult>` | Xin quyền foreground.      |
| `requestAlwaysLocationPermissions()` | `Future<String>`           | Xin quyền background.      |
| `hasLocationPermissions()`           | `Future<PermissionResult>` | Kiểm tra quyền, không popup. |

**Fake GPS**

| Method                                           | Return             | Mô tả                             |
| ------------------------------------------------ | ------------------ | --------------------------------- |
| `setFakeGpsPolicy(policy)`                       | `Future<void>`     | Đặt chính sách xử lý GPS giả.     |
| `setFakeGpsNotificationConfig({title, message})` | `Future<void>`     | Nội dung notification GPS giả.    |
| `onFakeGpsDetected`                              | `Stream<FakeGpsEvent>` | Stream sự kiện GPS giả.       |

**Tracking Interrupted**

| Method                                                        | Return                            | Mô tả                                |
| ------------------------------------------------------------- | --------------------------------- | ------------------------------------ |
| `setTrackingInterruptedNotificationEnabled(enabled)`          | `Future<void>`                    | Bật/tắt notification gián đoạn.      |
| `setTrackingInterruptedNotificationConfig({title, message})`  | `Future<void>`                    | Nội dung notification gián đoạn.     |
| `onTrackingInterrupted`                                       | `Stream<TrackingInterruptedEvent>` | Stream sự kiện gián đoạn / khôi phục. |

**Cảnh báo tốc độ**

| Method                             | Return         | Mô tả                            |
| ---------------------------------- | -------------- | -------------------------------- |
| `configureAlertAPI(apiKey, apiID)` | `Future<void>` | Cấu hình Alert API.              |
| `turnOnAlert()`                    | `Future<void>` | Bật cảnh báo tốc độ.             |
| `turnOffAlert()`                   | `Future<void>` | Tắt cảnh báo tốc độ.             |

**Smart Battery**

| Method                                      | Return         | Mô tả                                        |
| ------------------------------------------- | -------------- | -------------------------------------------- |
| `setSmartBatteryPreferredProfile(profile)`  | `void`         | Đặt profile ưu tiên khi đang di chuyển.      |
| `feedLocationToSmartBattery(speed, {heading})` | `void`      | Cấp tốc độ/hướng để phát hiện đứng yên & vào cua. |
| `enableSmartBatteryOptimization()`          | `Future<void>` | Bật Smart Battery giữa phiên.                |
| `disableSmartBatteryOptimization()`         | `void`         | Tắt Smart Battery (teardown phía Dart).      |
| `onSmartBatteryProfileChanged`              | `Stream<SmartBatteryProfile>` | Stream thay đổi profile.      |

**Cache & Đồng bộ**

| Method                              | Return         | Mô tả                        |
| ----------------------------------- | -------------- | ---------------------------- |
| `setAutoUpload(enabled)`            | `Future<bool>` | Bật/tắt auto upload.         |
| `isNetworkConnected()`              | `Future<bool>` | Kiểm tra kết nối mạng.       |
| `getCachedLocationsCount()`         | `Future<int>`  | Số records đang chờ gửi.     |
| `getDatabaseSizeBytes()`            | `Future<int>`  | Kích thước cache (bytes).    |
| `uploadCachedLocationsManually()`   | `Future<bool>` | Flush cache ngay.            |
| `clearCachedLocations()`            | `Future<bool>` | Xoá toàn bộ cache.           |
| `configureCacheLimits(...)`         | `Future<bool>` | Cấu hình giới hạn cache.     |

**Lịch sử**

| Method                                                                                          | Return                     | Mô tả                        |
| ----------------------------------------------------------------------------------------------- | -------------------------- | ---------------------------- |
| `getTrackingHistory({userId, fromTimestamp, toTimestamp, pageNumber, pageSize, sortDescending})` | `Future<List<GpsLocation>>` | Lấy lịch sử GPS theo bộ lọc. |

---

## 21. Kiến trúc

```
┌──────────────────────────────────────────────────┐
│                   Flutter App                    │
│          VietmapTrackingController               │
│          SmartBatteryManager                     │
├──────────────────────────────────────────────────┤
│           Platform Interface (Dart)              │
│    MethodChannel: vietmap_tracking_plugin        │
│    EventChannel:  /location_updates              │
│    EventChannel:  /tracking_status               │
│    EventChannel:  /fake_gps                      │
├──────────────────────┬───────────────────────────┤
│   iOS Native Bridge  │  Android Native Bridge    │
│   Swift              │  Kotlin                   │
├──────────────────────┼───────────────────────────┤
│  VietmapTrackingSDK  │  vietmap-tracking-sdk     │
│  1.5.1 (CocoaPods)   │  1.5.2 (JitPack)          │
└──────────────────────┴───────────────────────────┘
```

---

## 22. Xử lý sự cố

### Android

**Tracking nền bị dừng bất thường**

- Khai báo đủ `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_LOCATION` trong `AndroidManifest.xml`.
- Loại app khỏi danh sách tối ưu pin trong cài đặt máy.
- Trên Xiaomi / Huawei / Samsung, phải bật thủ công quyền chạy nền trong cài đặt hệ thống.

**Không thấy notification GPS giả**

- Đảm bảo `POST_NOTIFICATIONS` có trong manifest và đã được cấp runtime (Android 13+).
- Trên MIUI: Cài đặt → Ứng dụng → [App] → Thông báo → bật "Floating notifications".

**Build Gradle lỗi — không tìm thấy SDK**

- Kiểm tra `maven { url 'https://jitpack.io' }` nằm trong `allprojects { repositories { ... } }`.

### iOS

**Vị trí ngừng cập nhật khi chạy nền**

- `UIBackgroundModes` phải chứa `location`.
- Người dùng phải cấp quyền "Always".
- `backgroundMode: true` phải được set trong `LocationTrackingConfig`.

**Banner notification không hiện khi app đang mở**

- Kiểm tra `UNUserNotificationCenter.current().delegate = self` được gán **trước** `GeneratedPluginRegistrant.register`.
- Override `willPresent` phải gọi `completionHandler([.banner, .sound, .badge])` — **không** gọi `super`.

**`pod install` thất bại**

- Chạy `pod repo update` rồi `pod install`.
- `Podfile` phải khai báo `platform :ios, '15.0'` trở lên.

**`FileSystemException: Failed to decode data using encoding 'utf-8'`**

- Xảy ra khi project nằm trên phân vùng không phải APFS (ví dụ ExFAT). Thêm vào `ios/Podfile`:

```ruby
post_install do |installer|
  Dir.glob(File.join(installer.sandbox.root, '**', '._*')).each { |f| FileUtils.rm_f(f) }
end
```
