Backup before removing hiddify references
1
.fvm/flutter_sdk
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/vodorod/fvm/versions/3.24.0
|
||||||
3
.fvm/fvm_config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"flutterSdkVersion": "3.24.0"
|
||||||
|
}
|
||||||
1
.fvm/release
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.24.0
|
||||||
0
.fvm/version
Normal file
1
.fvm/versions/3.24.0
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/vodorod/fvm/versions/3.24.0
|
||||||
2
.gitignore
vendored
@@ -59,4 +59,4 @@ app.*.map.json
|
|||||||
/data
|
/data
|
||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/lib/core/telegram_config.dart
|
||||||
|
|||||||
306
AUDIT_REPORT.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# 📋 Аудит проекта Umbrix v0.1.0 (build 100)
|
||||||
|
|
||||||
|
**Дата:** 29 декабря 2025 г.
|
||||||
|
**Flutter:** 3.24.0 (Stable)
|
||||||
|
**Платформа:** Android SDK 36.1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Статус сборки
|
||||||
|
|
||||||
|
### 🎯 Успешно собрано
|
||||||
|
- **APK:** `build/app/outputs/flutter-apk/app-debug.apk`
|
||||||
|
- **Размер:** 223 MB (debug-режим с символами отладки)
|
||||||
|
- **Время сборки:** 11.4s (Gradle)
|
||||||
|
- **Bootstrap:** 2358ms
|
||||||
|
- **Статус:** ✅ Приложение запущено и работает на эмуляторе
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Статический анализ кода (Flutter Analyze)
|
||||||
|
|
||||||
|
### Общая статистика
|
||||||
|
- **Всего проблем:** 265
|
||||||
|
- **Категории:**
|
||||||
|
- ❌ **Errors:** 0 (критических ошибок нет)
|
||||||
|
- ⚠️ **Warnings:** ~30 (неиспользуемые импорты, мёртвый код)
|
||||||
|
- ℹ️ **Info/Hints:** ~235 (стилистика, рекомендации)
|
||||||
|
|
||||||
|
### Основные проблемы
|
||||||
|
|
||||||
|
#### ⚠️ Warnings (требуют внимания):
|
||||||
|
1. **Неиспользуемые импорты** (20+ файлов):
|
||||||
|
- `lib/features/per_app_proxy/overview/per_app_proxy_page.dart:5` - `go_router`
|
||||||
|
- `lib/features/home/widget/home_page.dart:15` - `active_proxy_notifier.dart`
|
||||||
|
- `lib/features/settings/widgets/advanced_setting_tiles.dart` - 3 импорта
|
||||||
|
- И другие...
|
||||||
|
|
||||||
|
2. **Неиспользуемые переменные**:
|
||||||
|
- `connection_button.dart:31` - `today`
|
||||||
|
- `system_tray_notifier.dart:64` - `destinations`
|
||||||
|
- `logs_overview_page.dart:31` - `debug`
|
||||||
|
- `about_page.dart:28` - `appUpdate`
|
||||||
|
- И другие...
|
||||||
|
|
||||||
|
3. **Мёртвый код (Dead code)**:
|
||||||
|
- `profile_notifier.dart:117` - `false && ...` (всегда false)
|
||||||
|
- `qr_code_scanner_screen.dart:143` - недостижимый код
|
||||||
|
- `system_tray_notifier.dart:184` - недостижимый код
|
||||||
|
|
||||||
|
4. **Go lang warning**:
|
||||||
|
- `libcore/config/server.go:46` - impossible condition: `nil != nil`
|
||||||
|
|
||||||
|
#### ℹ️ Info (рекомендации по стилю):
|
||||||
|
- Отсутствие `const` конструкторов (~100 мест)
|
||||||
|
- Отсутствие trailing commas (~50 мест)
|
||||||
|
- Использование deprecated API (~10 мест)
|
||||||
|
- Generated код (protobuf) - игнорируется
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Зависимости
|
||||||
|
|
||||||
|
### Устаревшие пакеты (требующие обновления):
|
||||||
|
|
||||||
|
#### Критичные (серьёзно устарели):
|
||||||
|
- `flutter_adaptive_scaffold`: 0.1.12 → **0.3.3+1** (discontinued! ⚠️)
|
||||||
|
- `go_router`: 13.2.5 → **17.0.1** (+3 мажорные версии)
|
||||||
|
- `grpc`: 3.2.4 → **5.1.0** (+2 мажорные версии)
|
||||||
|
- `hooks_riverpod`: 2.6.1 → **3.1.0**
|
||||||
|
- `freezed_annotation`: 2.4.4 → **3.1.0**
|
||||||
|
- `riverpod_annotation`: 2.6.1 → **4.0.0**
|
||||||
|
- `protobuf`: 3.1.0 → **6.0.0** (+3 мажорные версии)
|
||||||
|
|
||||||
|
#### Рекомендуемые к обновлению:
|
||||||
|
- `mobile_scanner`: 5.2.3 → 7.1.4
|
||||||
|
- `package_info_plus`: 5.0.1 → 9.0.0
|
||||||
|
- `sentry_flutter`: 7.20.2 → 9.9.1
|
||||||
|
- `share_plus`: 7.2.2 → 12.0.1
|
||||||
|
- `slang`: 3.32.0 → 4.11.1
|
||||||
|
- `upgrader`: 9.0.0 → 12.3.0
|
||||||
|
- `window_manager`: 0.3.9 → 0.5.1
|
||||||
|
- `wolt_modal_sheet`: 0.4.1 → 0.11.0
|
||||||
|
|
||||||
|
#### Dev dependencies:
|
||||||
|
- `build_runner`: 2.4.13 → 2.10.4
|
||||||
|
- `dependency_validator`: 3.2.3 → 5.0.3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Конфигурация линтера
|
||||||
|
|
||||||
|
**Файл:** `analysis_options.yaml`
|
||||||
|
|
||||||
|
### Используемые плагины:
|
||||||
|
- ✅ `package:lint/strict.yaml` (строгие правила)
|
||||||
|
- ✅ `custom_lint` с `provider_parameters`
|
||||||
|
|
||||||
|
### Исключения:
|
||||||
|
- `libcore/**` (Go код)
|
||||||
|
- `**.g.dart` (сгенерированный код)
|
||||||
|
- `lib/gen/**` (переводы)
|
||||||
|
|
||||||
|
### Отключенные правила:
|
||||||
|
- `sort_pub_dependencies: false`
|
||||||
|
- `sort_unnamed_constructors_first: false`
|
||||||
|
- `avoid_classes_with_only_static_members: false`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Переводы (i18n)
|
||||||
|
|
||||||
|
### Статус: ✅ Полная локализация
|
||||||
|
|
||||||
|
**Поддерживаемые языки:** 11
|
||||||
|
- 🇸🇦 ar (арабский)
|
||||||
|
- 🇬🇧 en (английский - базовый)
|
||||||
|
- 🇪🇸 es (испанский)
|
||||||
|
- 🇮🇷 fa (персидский)
|
||||||
|
- 🇫🇷 fr (французский)
|
||||||
|
- 🇮🇩 id (индонезийский)
|
||||||
|
- 🇧🇷 pt-BR (португальский)
|
||||||
|
- 🇷🇺 ru (русский) ✨
|
||||||
|
- 🇹🇷 tr (турецкий)
|
||||||
|
- 🇨🇳 zh-CN (китайский упрощенный)
|
||||||
|
- 🇹🇼 zh-TW (китайский традиционный)
|
||||||
|
|
||||||
|
**Генератор:** slang v3.32.0 (можно обновить до 4.11.1)
|
||||||
|
**Время генерации:** 0.229s
|
||||||
|
**Статус:** Все ключи переведены, включая:
|
||||||
|
- ✅ `excludedDomains` (добавлены для id, pt-BR, zh-TW в этой сессии)
|
||||||
|
- ✅ `proxies.pageTitle` изменено с "Proxies" на "Locations" во всех языках
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Flutter Doctor
|
||||||
|
|
||||||
|
### ✅ Работающие компоненты:
|
||||||
|
- Flutter SDK 3.24.0 (stable)
|
||||||
|
- Android toolchain SDK 36.1.0
|
||||||
|
- Android Studio 2025.1.3
|
||||||
|
- VS Code 1.106.3 / 1.108.0-insider
|
||||||
|
- Connected devices (2)
|
||||||
|
- Network resources
|
||||||
|
|
||||||
|
### ❌ Отсутствующие (для полной поддержки):
|
||||||
|
- Chrome (для web-разработки)
|
||||||
|
- Linux toolchain (clang++, CMake, ninja)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Git Status
|
||||||
|
|
||||||
|
### Изменённые файлы:
|
||||||
|
- `.gitignore`
|
||||||
|
- `android/app/build.gradle`
|
||||||
|
- `android/app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
### Удалённые файлы (ребрендинг):
|
||||||
|
- Старый пакет: `com.hiddify.hiddify`
|
||||||
|
- Новый пакет: `com.umbrix.app`
|
||||||
|
- Удалено ~50 Kotlin файлов из старого namespace
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI Изменения (текущая сессия)
|
||||||
|
|
||||||
|
1. **Размер шрифта:**
|
||||||
|
- Description: 12px (было: default)
|
||||||
|
- Buttons: 13px (было: default)
|
||||||
|
- Цель: уместить русский текст в одну строку
|
||||||
|
|
||||||
|
2. **Переводы:**
|
||||||
|
- Кнопка "Proxies" → "Локации" (все языки)
|
||||||
|
- Добавлен раздел "Exclusions" для id, pt-BR, zh-TW
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Рекомендации
|
||||||
|
|
||||||
|
### 🔴 Критично (сделать в ближайшее время):
|
||||||
|
|
||||||
|
1. **Удалить неиспользуемые импорты:**
|
||||||
|
```bash
|
||||||
|
dart fix --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Обновить discontinued пакет:**
|
||||||
|
- Заменить `flutter_adaptive_scaffold` на актуальную альтернативу
|
||||||
|
|
||||||
|
3. **Исправить мёртвый код:**
|
||||||
|
- `profile_notifier.dart:117` - убрать `false &&`
|
||||||
|
- Другие dead code warnings
|
||||||
|
|
||||||
|
### 🟡 Важно (планировать):
|
||||||
|
|
||||||
|
4. **Major updates пакетов:**
|
||||||
|
- Обновить Riverpod 2.x → 3.x (breaking changes!)
|
||||||
|
- Обновить go_router 13.x → 17.x
|
||||||
|
- Обновить protobuf 3.x → 6.x
|
||||||
|
- Обновить grpc 3.x → 5.x
|
||||||
|
|
||||||
|
5. **Размер APK (223 MB):**
|
||||||
|
- Создать release build для оптимизации
|
||||||
|
- Проверить, нет ли лишних ресурсов
|
||||||
|
- Использовать app bundle вместо APK
|
||||||
|
|
||||||
|
### 🟢 Желательно (улучшения):
|
||||||
|
|
||||||
|
6. **Добавить const конструкторы** (производительность)
|
||||||
|
7. **Добавить trailing commas** (читаемость)
|
||||||
|
8. **Обновить deprecated API:**
|
||||||
|
- `AutoDisposeRef` → `Ref`
|
||||||
|
- `SingboxServiceRef` → `Ref`
|
||||||
|
- `package:drift_dev/api/migrations.dart`
|
||||||
|
|
||||||
|
9. **Linux support:**
|
||||||
|
```bash
|
||||||
|
sudo apt install clang cmake ninja-build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Плагины для проверки
|
||||||
|
|
||||||
|
### Уже используются:
|
||||||
|
- ✅ `flutter analyze` (встроенный статический анализатор)
|
||||||
|
- ✅ `custom_lint` (кастомные правила для Riverpod)
|
||||||
|
- ✅ `package:lint/strict.yaml` (строгие правила)
|
||||||
|
|
||||||
|
### Рекомендуемые дополнительно:
|
||||||
|
|
||||||
|
1. **dart_code_metrics** (DCM)
|
||||||
|
```yaml
|
||||||
|
dev_dependencies:
|
||||||
|
dart_code_metrics: ^5.7.6
|
||||||
|
```
|
||||||
|
- Циклическая сложность
|
||||||
|
- Метрики кода
|
||||||
|
- Anti-patterns
|
||||||
|
|
||||||
|
2. **flutter_lints**
|
||||||
|
```yaml
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_lints: ^4.0.0
|
||||||
|
```
|
||||||
|
- Более современная альтернатива lint
|
||||||
|
|
||||||
|
3. **very_good_analysis**
|
||||||
|
```yaml
|
||||||
|
dev_dependencies:
|
||||||
|
very_good_analysis: ^5.1.0
|
||||||
|
```
|
||||||
|
- Еще более строгие правила от Very Good Ventures
|
||||||
|
|
||||||
|
4. **Dependency checkers:**
|
||||||
|
```bash
|
||||||
|
flutter pub run dependency_validator
|
||||||
|
```
|
||||||
|
- Проверка неиспользуемых зависимостей
|
||||||
|
|
||||||
|
5. **Security audit:**
|
||||||
|
```bash
|
||||||
|
flutter pub run pubspec_dependency_analyzer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Итоговая оценка
|
||||||
|
|
||||||
|
| Критерий | Оценка | Комментарий |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| **Сборка** | ✅ 10/10 | Собирается без ошибок |
|
||||||
|
| **Код-стиль** | ⚠️ 6/10 | 265 замечаний, но нет критичных |
|
||||||
|
| **Зависимости** | ⚠️ 5/10 | Много устаревших пакетов |
|
||||||
|
| **Локализация** | ✅ 10/10 | 11 языков, все ключи переведены |
|
||||||
|
| **Размер APK** | ⚠️ 6/10 | 223 MB (debug), нужен release |
|
||||||
|
| **Тесты** | ❓ N/A | Требует проверки |
|
||||||
|
| **Документация** | ✅ 8/10 | README есть, можно улучшить |
|
||||||
|
|
||||||
|
### Общая оценка: ⚠️ 7.5/10
|
||||||
|
|
||||||
|
**Вывод:** Проект в хорошем состоянии, основные проблемы - устаревшие зависимости и стилистические замечания линтера. Критических ошибок нет, приложение работает стабильно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 План действий
|
||||||
|
|
||||||
|
### Этап 1 (Немедленно):
|
||||||
|
- [ ] Запустить `dart fix --apply`
|
||||||
|
- [ ] Удалить неиспользуемые импорты вручную
|
||||||
|
- [ ] Исправить dead code
|
||||||
|
|
||||||
|
### Этап 2 (На этой неделе):
|
||||||
|
- [ ] Собрать release APK
|
||||||
|
- [ ] Проверить размер release build
|
||||||
|
- [ ] Обновить minor версии пакетов
|
||||||
|
|
||||||
|
### Этап 3 (Планирование):
|
||||||
|
- [ ] Протестировать major updates
|
||||||
|
- [ ] Заменить discontinued пакеты
|
||||||
|
- [ ] Добавить DCM метрики
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Отчёт сгенерирован:** GitHub Copilot
|
||||||
|
**Версия приложения:** Umbrix v0.1.0 (100)
|
||||||
172
DOMAIN_ZONES_AUTO_SELECTION.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# 🌍 Автоматический выбор доменных зон по региону
|
||||||
|
|
||||||
|
## ✅ Что добавлено
|
||||||
|
|
||||||
|
### 1. Расширенный список доменных зон
|
||||||
|
|
||||||
|
**По регионам** (автоматически показываются первыми):
|
||||||
|
|
||||||
|
#### 🇷🇺 Россия и СНГ (Region.ru):
|
||||||
|
- `.ru`, `.рф`, `.su` - Россия
|
||||||
|
- `.by` - Беларусь
|
||||||
|
- `.kz` - Казахстан
|
||||||
|
- `.ua` - Украина
|
||||||
|
- `.am` - Армения
|
||||||
|
- `.ge` - Грузия
|
||||||
|
- `.md` - Молдова
|
||||||
|
- `.kg` - Киргизия
|
||||||
|
- `.uz` - Узбекистан
|
||||||
|
- `.tm` - Туркменистан
|
||||||
|
- `.az` - Азербайджан
|
||||||
|
|
||||||
|
#### 🇮🇷 Иран и окружение (Region.ir):
|
||||||
|
- `.ir`, `.ایران` - Иран
|
||||||
|
- `.af` - Афганистан
|
||||||
|
- `.tj`, `.تاجیکستان` - Таджикистан
|
||||||
|
- `.pk` - Пакистан
|
||||||
|
- `.iq` - Ирак
|
||||||
|
|
||||||
|
#### 🇨🇳 Китай и Восточная Азия (Region.cn):
|
||||||
|
- `.cn`, `.中国` - Китай
|
||||||
|
- `.hk` - Гонконг
|
||||||
|
- `.tw` - Тайвань
|
||||||
|
- `.mo` - Макао
|
||||||
|
- `.sg` - Сингапур
|
||||||
|
- `.kr` - Южная Корея
|
||||||
|
- `.jp` - Япония
|
||||||
|
|
||||||
|
#### 🇮🇩 Индонезия и Юго-Восточная Азия (Region.id):
|
||||||
|
- `.id` - Индонезия
|
||||||
|
- `.my` - Малайзия
|
||||||
|
- `.ph` - Филиппины
|
||||||
|
- `.vn` - Вьетнам
|
||||||
|
- `.th` - Таиланд
|
||||||
|
- `.la` - Лаос
|
||||||
|
- `.mm` - Мьянма
|
||||||
|
- `.kh` - Камбоджа
|
||||||
|
- `.bn` - Бруней
|
||||||
|
- `.tl` - Восточный Тимор
|
||||||
|
|
||||||
|
#### 🇹🇷 Турция и Тюркский мир (Region.tr):
|
||||||
|
- `.tr` - Турция
|
||||||
|
- `.az` - Азербайджан
|
||||||
|
- `.tm` - Туркменистан
|
||||||
|
- `.uz` - Узбекистан
|
||||||
|
- `.kg` - Киргизия
|
||||||
|
- `.kz` - Казахстан
|
||||||
|
|
||||||
|
#### 🇦🇫 Афганистан и окружение (Region.af):
|
||||||
|
- `.af` - Афганистан
|
||||||
|
- `.pk` - Пакистан
|
||||||
|
- `.tj`, `.تاجیکستان` - Таджикистан
|
||||||
|
- `.ir`, `.ایران` - Иран
|
||||||
|
|
||||||
|
#### 🇧🇷 Бразилия и Латинская Америка (Region.br):
|
||||||
|
- `.br` - Бразилия
|
||||||
|
- `.pt` - Португалия
|
||||||
|
- `.ao` - Ангола
|
||||||
|
- `.mz` - Мозамбик
|
||||||
|
- `.mx` - Мексика
|
||||||
|
- `.ar` - Аргентина
|
||||||
|
- `.cl` - Чили
|
||||||
|
- `.co` - Колумбия
|
||||||
|
- `.ve` - Венесуэла
|
||||||
|
- `.pe` - Перу
|
||||||
|
|
||||||
|
#### 🇮🇳 Индия и Южная Азия (Region.in_) **[НОВЫЙ РЕГИОН]**:
|
||||||
|
- `.in`, `.भारत` - Индия
|
||||||
|
- `.pk` - Пакистан
|
||||||
|
- `.bd` - Бангладеш
|
||||||
|
- `.lk` - Шри-Ланка
|
||||||
|
- `.np` - Непал
|
||||||
|
- `.bt` - Бутан
|
||||||
|
- `.mv` - Мальдивы
|
||||||
|
|
||||||
|
### 2. Глобальные популярные зоны (топ-20):
|
||||||
|
|
||||||
|
Всегда показываются после региональных:
|
||||||
|
- `.com`, `.org`, `.net`, `.info`, `.biz`
|
||||||
|
- `.co`, `.io`, `.ai`, `.app`, `.dev`
|
||||||
|
- `.xyz`, `.online`, `.site`, `.tech`, `.store`
|
||||||
|
- `.me`, `.cc`, `.tv`, `.pro`, `.us`
|
||||||
|
|
||||||
|
## 🤖 Как работает автоматический выбор
|
||||||
|
|
||||||
|
1. **При открытии "Исключения"** → **"+ Добавить домены"**:
|
||||||
|
- Система определяет регион пользователя (Region.ru, Region.cn, и т.д.)
|
||||||
|
- Автоматически показывает релевантные зоны **в начале списка**
|
||||||
|
- Затем добавляет популярные глобальные зоны
|
||||||
|
|
||||||
|
2. **Пример для русского пользователя** (Region.ru):
|
||||||
|
```
|
||||||
|
[✓] .ru
|
||||||
|
[✓] .рф
|
||||||
|
[ ] .su
|
||||||
|
[ ] .by
|
||||||
|
[ ] .kz
|
||||||
|
[ ] .ua
|
||||||
|
... (11 региональных зон)
|
||||||
|
[ ] .com
|
||||||
|
[ ] .org
|
||||||
|
[ ] .net
|
||||||
|
... (20 глобальных зон)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Пример для китайского пользователя** (Region.cn):
|
||||||
|
```
|
||||||
|
[ ] .cn
|
||||||
|
[ ] .中国
|
||||||
|
[ ] .hk
|
||||||
|
[ ] .tw
|
||||||
|
... (8 региональных зон)
|
||||||
|
[ ] .com
|
||||||
|
[ ] .org
|
||||||
|
... (глобальные зоны)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 Измененные файлы
|
||||||
|
|
||||||
|
### 1. `lib/core/model/region.dart`
|
||||||
|
- ✅ Добавлен новый регион `in_` (Индия)
|
||||||
|
- Теперь всего **9 регионов** (было 8)
|
||||||
|
|
||||||
|
### 2. `lib/features/per_app_proxy/overview/per_app_proxy_page.dart`
|
||||||
|
- ✅ Добавлены импорты: `Region`, `ConfigOptions`
|
||||||
|
- ✅ Реализована логика автоматического выбора зон по региону
|
||||||
|
- ✅ Расширен список с 6 до **80+ доменных зон**
|
||||||
|
- ✅ Зоны группируются: региональные (по текущему региону) + глобальные
|
||||||
|
|
||||||
|
## 🎯 Преимущества
|
||||||
|
|
||||||
|
1. **Удобство для пользователей**:
|
||||||
|
- Не нужно искать нужные зоны вручную
|
||||||
|
- Релевантные зоны показываются первыми
|
||||||
|
- Поддержка национальных доменов (кириллица, арабский, китайский)
|
||||||
|
|
||||||
|
2. **Полнота покрытия**:
|
||||||
|
- Россия: 13 зон (было 6)
|
||||||
|
- Китай: 8 зон (было 6)
|
||||||
|
- Иран: 7 зон (было 5)
|
||||||
|
- Индонезия: 10 зон (было 6)
|
||||||
|
- **Новый регион**: Индия (7 зон)
|
||||||
|
|
||||||
|
3. **Умное поведение**:
|
||||||
|
- Автоматически определяет регион при первом запуске
|
||||||
|
- Можно вручную изменить регион в настройках
|
||||||
|
- Список обновляется при смене региона
|
||||||
|
|
||||||
|
## 📱 Как протестировать
|
||||||
|
|
||||||
|
1. Запусти приложение
|
||||||
|
2. Открой: **Настройки** → **Сеть** → **Исключения** → **Домены**
|
||||||
|
3. Нажми **"+ Добавить домены"**
|
||||||
|
4. Увидишь:
|
||||||
|
- Для России: `.ru`, `.рф`, `.su`, `.by`, `.kz`, `.ua`, ... (13 зон)
|
||||||
|
- Затем глобальные: `.com`, `.org`, `.net`, `.io`, `.ai`, ... (20 зон)
|
||||||
|
|
||||||
|
## 🔮 Будущие улучшения
|
||||||
|
|
||||||
|
- [ ] Добавить автоматическую подстановку зон при первом запуске
|
||||||
|
- [ ] Сделать "быструю кнопку" для добавления всех региональных зон
|
||||||
|
- [ ] Показывать флаги стран рядом с зонами
|
||||||
|
- [ ] Добавить поиск по доменным зонам
|
||||||
354
SECURITY_AUDIT_REPORT.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# 🔒 ОТЧЁТ ПО БЕЗОПАСНОСТИ И УТЕЧКАМ ДАННЫХ
|
||||||
|
## Hiddify App v2.5.7 - Полный Аудит
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📧 КОНТАКТЫ ОРИГИНАЛЬНОГО ПРОЕКТА
|
||||||
|
|
||||||
|
### Email адреса разработчиков:
|
||||||
|
- **contribute@hiddify.com** - основной контакт для контрибьюторов
|
||||||
|
- **linux@hiddify.com** - для Linux пакетов (deb/rpm)
|
||||||
|
- **wrt@hiddify.com** - для OpenWRT роутеров
|
||||||
|
|
||||||
|
### Ссылки на проект:
|
||||||
|
- **GitHub**: https://github.com/hiddify/hiddify-next
|
||||||
|
- **Telegram**: https://t.me/hiddify
|
||||||
|
- **API релизов**: https://api.github.com/repos/hiddify/hiddify-next/releases
|
||||||
|
- **Сайт**: https://hiddify.com/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 КРИТИЧЕСКИЕ НАХОДКИ
|
||||||
|
|
||||||
|
### 1. ⚠️ SENTRY - АВТОМАТИЧЕСКАЯ ОТПРАВКА КРАШЕЙ
|
||||||
|
|
||||||
|
**Файл**: `lib/core/analytics/analytics_controller.dart`
|
||||||
|
|
||||||
|
**Что делает**:
|
||||||
|
- Собирает информацию о крашах приложения
|
||||||
|
- Отправляет стек-трейсы ошибок на сервера Sentry
|
||||||
|
- Включает логи, информацию об окружении, версию приложения
|
||||||
|
|
||||||
|
**Куда отправляет**:
|
||||||
|
```dart
|
||||||
|
final dsn = !kDebugMode || _testCrashReport ? Environment.sentryDSN : "";
|
||||||
|
```
|
||||||
|
DSN (Data Source Name) берётся из переменной окружения `sentry_dsn` при сборке.
|
||||||
|
|
||||||
|
**Какие данные собираются**:
|
||||||
|
```dart
|
||||||
|
SentryFlutter.init(
|
||||||
|
(options) {
|
||||||
|
options.dsn = dsn;
|
||||||
|
options.environment = env.name; // prod/dev
|
||||||
|
options.dist = appInfo.release.name; // версия релиза
|
||||||
|
options.enableNativeCrashHandling = true; // крэши нативного кода
|
||||||
|
options.enableNdkScopeSync = true; // Android NDK
|
||||||
|
options.serverName = ""; // имя сервера (пустое)
|
||||||
|
options.attachThreads = true; // информация о потоках
|
||||||
|
options.tracesSampleRate = 0.20; // 20% трассировка производительности
|
||||||
|
options.enableUserInteractionTracing = true; // отслеживание действий пользователя
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ ВАЖНО**:
|
||||||
|
- Пользовательские данные **АНОНИМИЗИРОВАНЫ**:
|
||||||
|
```dart
|
||||||
|
event.copyWith(
|
||||||
|
user: SentryUser(email: "", username: "", ipAddress: "0.0.0.0"),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
- Но всё равно отправляются: стек-трейсы, версия приложения, операционная система, действия перед крашем
|
||||||
|
|
||||||
|
**✅ ХОРОШАЯ НОВОСТЬ**:
|
||||||
|
- Можно отключить в настройках!
|
||||||
|
- По умолчанию **ВКЛЮЧЕНО** (`true`)
|
||||||
|
- Файл: `lib/core/analytics/analytics_controller.dart:23`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 🌍 АВТОМАТИЧЕСКОЕ ОПРЕДЕЛЕНИЕ СТРАНЫ
|
||||||
|
|
||||||
|
**Файл**: `lib/features/intro/widget/intro_page.dart:275`
|
||||||
|
|
||||||
|
**Что делает**:
|
||||||
|
При первом запуске приложение **АВТОМАТИЧЕСКИ** отправляет запрос к внешнему сервису:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final response = await client.get<Map<String, dynamic>>('https://api.ip.sb/geoip/');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Какие данные отправляются**:
|
||||||
|
- Ваш **IP адрес** (автоматически виден серверу)
|
||||||
|
- User-Agent: `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0`
|
||||||
|
|
||||||
|
**Что получает приложение**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"country_code": "RU",
|
||||||
|
"country": "Russia",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Цель**: Автоматически выбрать язык интерфейса и регион настроек.
|
||||||
|
|
||||||
|
**⚠️ РИСК**:
|
||||||
|
- Ваш IP адрес становится известен стороннему сервису **api.ip.sb**
|
||||||
|
- Происходит **БЕЗ СОГЛАСИЯ** пользователя при первом запуске
|
||||||
|
- Сервис может логировать IP адреса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 🔄 АВТОМАТИЧЕСКАЯ ПРОВЕРКА ОБНОВЛЕНИЙ
|
||||||
|
|
||||||
|
**Файл**: `lib/features/app_update/notifier/app_update_notifier.dart`
|
||||||
|
|
||||||
|
**Что делает**:
|
||||||
|
Приложение периодически проверяет наличие обновлений, обращаясь к:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://raw.githubusercontent.com/hiddify/hiddify-next/main/appcast.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файл appcast.xml**:
|
||||||
|
```xml
|
||||||
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||||
|
<channel>
|
||||||
|
<title>Hiddify</title>
|
||||||
|
<link>https://github.com/hiddify/hiddify-next/releases</link>
|
||||||
|
...
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Куда отправляется запрос**:
|
||||||
|
- GitHub Raw (через CDN)
|
||||||
|
- Может быть виден ваш IP адрес в логах GitHub/CDN
|
||||||
|
|
||||||
|
**Частота**: Раз в 12 часов (`durationUntilAlertAgain: Duration(hours: 12)`)
|
||||||
|
|
||||||
|
**⚠️ ПРИМЕЧАНИЕ**:
|
||||||
|
- Это стандартная практика для проверки обновлений
|
||||||
|
- Можно отключить для Google Play релиза (`allowCustomUpdateChecker`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 🌐 ПРОВЕРКА IP ИНФОРМАЦИИ ПРОКСИ
|
||||||
|
|
||||||
|
**Файл**: `lib/features/proxy/data/proxy_repository.dart:127-129`
|
||||||
|
|
||||||
|
Приложение использует **3 сервиса** для проверки текущего IP при подключении к VPN:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||||
|
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||||
|
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что отправляется**: Ваш IP адрес (через VPN, если подключен)
|
||||||
|
|
||||||
|
**Цель**: Показать пользователю текущее местоположение и провайдера
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ ЧТО НЕ СОБИРАЕТСЯ (ХОРОШИЕ НОВОСТИ)
|
||||||
|
|
||||||
|
✅ **НЕТ Firebase Analytics** (закомментировано в истории)
|
||||||
|
✅ **НЕТ Google Analytics**
|
||||||
|
✅ **НЕТ рекламных трекеров**
|
||||||
|
✅ **НЕТ сбора списка приложений** (кроме Per-App Proxy функции)
|
||||||
|
✅ **НЕТ доступа к контактам, SMS, звонкам**
|
||||||
|
✅ **НЕТ передачи VPN трафика третьим лицам**
|
||||||
|
✅ **НЕТ сбора истории браузера**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 НАСТРОЙКИ ПРИВАТНОСТИ В ПРИЛОЖЕНИИ
|
||||||
|
|
||||||
|
### Где найти:
|
||||||
|
|
||||||
|
**Экран приветствия** (`lib/features/intro/widget/intro_page.dart`):
|
||||||
|
```
|
||||||
|
"Сбор аналитики"
|
||||||
|
"Сбор данных аналитики и отправка отчетов о сбоях для улучшения приложения"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Переводы** (`assets/translations/strings_ru.i18n.json`):
|
||||||
|
```json
|
||||||
|
"enableAnalytics": "Включить аналитику",
|
||||||
|
"enableAnalyticsMsg": "Разрешить сбор аналитики и отправку отчетов о сбоях"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как работает:
|
||||||
|
|
||||||
|
1. **Первый запуск**: Пользователю предлагается включить/отключить аналитику
|
||||||
|
2. **Сохраняется в**: SharedPreferences (`enable_analytics` ключ)
|
||||||
|
3. **По умолчанию**: `true` (ВКЛЮЧЕНО)
|
||||||
|
4. **Можно изменить**: В настройках приложения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 РЕКОМЕНДАЦИИ ПО БЕЗОПАСНОСТИ
|
||||||
|
|
||||||
|
### Для обычных пользователей:
|
||||||
|
|
||||||
|
1. **Отключите аналитику** при первом запуске
|
||||||
|
2. Понимайте, что ваш IP виден при первом запросе к `api.ip.sb`
|
||||||
|
3. Используйте VPN сразу после установки, чтобы скрыть реальный IP
|
||||||
|
|
||||||
|
### Для параноиков:
|
||||||
|
|
||||||
|
1. **Заблокируйте** в файерволе:
|
||||||
|
- `api.ip.sb`
|
||||||
|
- `ipapi.co`
|
||||||
|
- `ipinfo.io`
|
||||||
|
- `sentry.io` (если отключили аналитику)
|
||||||
|
- `raw.githubusercontent.com` (отключит проверку обновлений)
|
||||||
|
|
||||||
|
2. **Измените код** перед сборкой:
|
||||||
|
- Удалите автоматический запрос к `api.ip.sb` (строка 275 в `intro_page.dart`)
|
||||||
|
- Установите `enableAnalyticsPrefKey` по умолчанию в `false`
|
||||||
|
|
||||||
|
3. **Соберите приложение без Sentry**:
|
||||||
|
```bash
|
||||||
|
flutter build apk --dart-define sentry_dsn=""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для разработчиков форка (Umbrix):
|
||||||
|
|
||||||
|
1. **Удалите все контакты Hiddify**:
|
||||||
|
- `contribute@hiddify.com` → замените на свои
|
||||||
|
- Telegram канал → замените на свой
|
||||||
|
- GitHub ссылки → замените на свой репозиторий
|
||||||
|
|
||||||
|
2. **Измените URLs**:
|
||||||
|
```dart
|
||||||
|
// lib/core/model/constants.dart
|
||||||
|
static const githubUrl = "https://github.com/YOUR_ACCOUNT/umbrix";
|
||||||
|
static const appCastUrl = "https://raw.githubusercontent.com/YOUR_ACCOUNT/umbrix/main/appcast.xml";
|
||||||
|
static const telegramChannelUrl = "https://t.me/YOUR_CHANNEL";
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Отключите Sentry** или используйте свой DSN:
|
||||||
|
```bash
|
||||||
|
# В Makefile замените или удалите:
|
||||||
|
SENTRY_DSN=your_sentry_dsn_here
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Замените IP определение** на приватное решение:
|
||||||
|
- Используйте только локальные методы (timezone, locale системы)
|
||||||
|
- Или предлагайте пользователю выбрать страну вручную
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 БЭКДОРЫ?
|
||||||
|
|
||||||
|
**НЕ ОБНАРУЖЕНО**. Код открытый, проверяется community на GitHub.
|
||||||
|
|
||||||
|
### Проверенные векторы:
|
||||||
|
|
||||||
|
- ✅ Нет скрытых серверов для коммуникации
|
||||||
|
- ✅ Нет зашифрованных payload
|
||||||
|
- ✅ Нет подозрительных native библиотек
|
||||||
|
- ✅ VPN трафик не перенаправляется на третьи сервера
|
||||||
|
- ✅ Нет обфусцированного кода
|
||||||
|
|
||||||
|
### Единственные внешние соединения:
|
||||||
|
|
||||||
|
1. **Sentry** (опционально, можно отключить)
|
||||||
|
2. **api.ip.sb** (при первом запуске)
|
||||||
|
3. **GitHub** (проверка обновлений)
|
||||||
|
4. **IP check сервисы** (при подключении к VPN, показывают текущий IP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 ИТОГОВАЯ ОЦЕНКА
|
||||||
|
|
||||||
|
### Уровень приватности: **7/10** ⭐⭐⭐⭐⭐⭐⭐☆☆☆
|
||||||
|
|
||||||
|
**Плюсы**:
|
||||||
|
- ✅ Открытый исходный код
|
||||||
|
- ✅ Можно отключить аналитику
|
||||||
|
- ✅ Данные анонимизированы в Sentry
|
||||||
|
- ✅ Нет рекламы и трекеров
|
||||||
|
- ✅ Нет сбора личных данных
|
||||||
|
|
||||||
|
**Минусы**:
|
||||||
|
- ⚠️ IP адрес утекает к `api.ip.sb` при первом запуске (БЕЗ СОГЛАСИЯ)
|
||||||
|
- ⚠️ Sentry включён по умолчанию
|
||||||
|
- ⚠️ Проверка обновлений с GitHub (виден IP)
|
||||||
|
|
||||||
|
**Вердикт**: Приложение относительно безопасное, но требует настройки для максимальной приватности.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ КАК УДАЛИТЬ ВСЕ УТЕЧКИ (ДЛЯ UMBRIX)
|
||||||
|
|
||||||
|
### 1. Удалите автоматическое определение страны:
|
||||||
|
|
||||||
|
**Файл**: `lib/features/intro/widget/intro_page.dart`
|
||||||
|
|
||||||
|
Замените функцию `autoSelectRegion` на:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> autoSelectRegion(WidgetRef ref) async {
|
||||||
|
// НЕ ДЕЛАЕМ НИЧЕГО - пусть пользователь выберет сам
|
||||||
|
loggy.debug("Auto region selection disabled for privacy");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Отключите Sentry по умолчанию:
|
||||||
|
|
||||||
|
**Файл**: `lib/core/analytics/analytics_controller.dart:23`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@override
|
||||||
|
Future<bool> build() async {
|
||||||
|
return _preferences.getBool(enableAnalyticsPrefKey) ?? false; // ← БЫЛО true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Удалите проверку обновлений:
|
||||||
|
|
||||||
|
**Файл**: `lib/features/app/widget/app.dart`
|
||||||
|
|
||||||
|
Закомментируйте или удалите:
|
||||||
|
```dart
|
||||||
|
// final upgrader = ref.watch(upgraderProvider);
|
||||||
|
// upgrader: upgrader,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Замените все URLs:
|
||||||
|
|
||||||
|
**Файл**: `lib/core/model/constants.dart`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class Constants {
|
||||||
|
static const appName = "Umbrix";
|
||||||
|
static const githubUrl = "https://github.com/YOUR_ACCOUNT/umbrix";
|
||||||
|
static const githubReleasesApiUrl =
|
||||||
|
"https://api.github.com/repos/YOUR_ACCOUNT/umbrix/releases";
|
||||||
|
static const githubLatestReleaseUrl =
|
||||||
|
"https://github.com/YOUR_ACCOUNT/umbrix/releases/latest";
|
||||||
|
static const appCastUrl =
|
||||||
|
"https://raw.githubusercontent.com/YOUR_ACCOUNT/umbrix/main/appcast.xml";
|
||||||
|
static const telegramChannelUrl = "https://t.me/YOUR_CHANNEL";
|
||||||
|
static const privacyPolicyUrl = "https://umbrix.com/privacy-policy/";
|
||||||
|
static const termsAndConditionsUrl = "https://umbrix.com/terms/";
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Соберите без Sentry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make TARGET=android SENTRY_DSN=""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата аудита**: 27 декабря 2025 г.
|
||||||
|
**Версия**: Hiddify v2.5.7
|
||||||
|
**Аудитор**: GitHub Copilot AI Assistant
|
||||||
161
TELEGRAM_BOT_SETUP.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 🤖 Настройка Telegram Bot для логов Umbrix
|
||||||
|
|
||||||
|
## 📝 Пошаговая инструкция
|
||||||
|
|
||||||
|
### 1️⃣ Создайте бота
|
||||||
|
|
||||||
|
1. Откройте **Telegram**
|
||||||
|
2. Найдите бота **@BotFather**
|
||||||
|
3. Отправьте команду: `/newbot`
|
||||||
|
4. Придумайте **имя** для бота (например: `Umbrix Logs Bot`)
|
||||||
|
5. Придумайте **username** (например: `umbrix_logs_bot`)
|
||||||
|
6. Скопируйте полученный **токен** (выглядит как `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz`)
|
||||||
|
|
||||||
|
### 2️⃣ Настройте приватность
|
||||||
|
|
||||||
|
1. Отправьте @BotFather команду: `/mybots`
|
||||||
|
2. Выберите своего бота
|
||||||
|
3. Нажмите `Bot Settings`
|
||||||
|
4. Нажмите `Group Privacy`
|
||||||
|
5. Нажмите `Turn OFF` (чтобы бот мог читать сообщения в группах)
|
||||||
|
|
||||||
|
### 3️⃣ Создайте канал/группу для логов
|
||||||
|
|
||||||
|
**Вариант A: Приватный канал (РЕКОМЕНДУЮ)**
|
||||||
|
1. Создайте новый канал в Telegram
|
||||||
|
2. Назовите его (например, "Umbrix Logs")
|
||||||
|
3. Сделайте канал **приватным**
|
||||||
|
4. Добавьте бота в администраторы канала
|
||||||
|
|
||||||
|
**Вариант B: Приватная группа**
|
||||||
|
1. Создайте новую группу
|
||||||
|
2. Добавьте бота в участники
|
||||||
|
3. Сделайте бота администратором
|
||||||
|
|
||||||
|
**Вариант C: Личные сообщения**
|
||||||
|
1. Найдите своего бота в Telegram
|
||||||
|
2. Нажмите `/start`
|
||||||
|
|
||||||
|
### 4️⃣ Получите Chat ID
|
||||||
|
|
||||||
|
**Способ 1: Через API (для каналов/групп)**
|
||||||
|
|
||||||
|
1. Отправьте любое сообщение в канал/группу
|
||||||
|
2. Откройте в браузере:
|
||||||
|
```
|
||||||
|
https://api.telegram.org/bot<ВАШ_ТОКЕН>/getUpdates
|
||||||
|
```
|
||||||
|
Замените `<ВАШ_ТОКЕН>` на токен от BotFather
|
||||||
|
|
||||||
|
3. Найдите в JSON ответе:
|
||||||
|
```json
|
||||||
|
"chat": {
|
||||||
|
"id": -1001234567890, ← ЭТО ВАШ CHAT ID
|
||||||
|
"title": "Umbrix Logs",
|
||||||
|
"type": "channel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Скопируйте этот ID (с минусом!)
|
||||||
|
|
||||||
|
**Способ 2: Через бота (для личных сообщений)**
|
||||||
|
|
||||||
|
1. Найдите бота **@userinfobot** в Telegram
|
||||||
|
2. Отправьте ему `/start`
|
||||||
|
3. Он пришлёт ваш Chat ID
|
||||||
|
|
||||||
|
### 5️⃣ Вставьте токены в код
|
||||||
|
|
||||||
|
Откройте файл: `lib/core/model/secrets.dart`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class Secrets {
|
||||||
|
static const String telegramBotToken = "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"; // ← ВАШ ТОКЕН
|
||||||
|
static const String telegramChatId = "-1001234567890"; // ← ВАШ CHAT ID
|
||||||
|
|
||||||
|
static bool get isConfigured =>
|
||||||
|
telegramBotToken.isNotEmpty && telegramChatId.isNotEmpty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6️⃣ Добавьте в .gitignore
|
||||||
|
|
||||||
|
**ОБЯЗАТЕЛЬНО!** Чтобы токен не попал в GitHub:
|
||||||
|
|
||||||
|
Откройте файл: `.gitignore`
|
||||||
|
|
||||||
|
Добавьте строку:
|
||||||
|
```
|
||||||
|
lib/core/model/secrets.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Или создайте secrets.dart с шаблоном:
|
||||||
|
```bash
|
||||||
|
cp lib/core/model/secrets.dart lib/core/model/secrets.example.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
В secrets.example.dart оставьте пустые строки, а secrets.dart добавьте в .gitignore.
|
||||||
|
|
||||||
|
### 7️⃣ Проверьте что работает
|
||||||
|
|
||||||
|
Запустите приложение и попробуйте отправить тестовый лог через настройки.
|
||||||
|
|
||||||
|
Если логи не приходят, проверьте:
|
||||||
|
- ✅ Токен правильный (скопирован полностью)
|
||||||
|
- ✅ Chat ID правильный (с минусом для каналов)
|
||||||
|
- ✅ Бот добавлен в администраторы канала
|
||||||
|
- ✅ Group Privacy выключен у бота
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Безопасность
|
||||||
|
|
||||||
|
### ❌ НЕ ДЕЛАЙТЕ ТАК:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ❌ НЕ хардкодьте токен напрямую!
|
||||||
|
const token = "1234567890:ABCdef...";
|
||||||
|
|
||||||
|
// ❌ НЕ коммитьте secrets.dart в Git!
|
||||||
|
git add lib/core/model/secrets.dart // НЕТ!
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ ПРАВИЛЬНО:
|
||||||
|
|
||||||
|
1. Храните токен в `secrets.dart`
|
||||||
|
2. Добавьте `secrets.dart` в `.gitignore`
|
||||||
|
3. Создайте `secrets.example.dart` с пустыми значениями для других разработчиков
|
||||||
|
4. В production сборке: используйте environment variables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📤 Формат логов
|
||||||
|
|
||||||
|
Бот будет отправлять сообщения в таком формате:
|
||||||
|
|
||||||
|
```
|
||||||
|
🐛 Отчёт об ошибке Umbrix
|
||||||
|
|
||||||
|
📱 Версия: 2.5.7
|
||||||
|
🤖 Android: 13 (API 33)
|
||||||
|
📦 Device: Samsung Galaxy S21
|
||||||
|
🆔 ID: a3f5c8d1 (анонимный)
|
||||||
|
|
||||||
|
📋 Логи:
|
||||||
|
[ERROR] Connection timeout
|
||||||
|
[WARN] Retry attempt 3/5
|
||||||
|
[INFO] Connecting to server...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Советы
|
||||||
|
|
||||||
|
1. **Используйте приватный канал** - логи могут содержать технические детали
|
||||||
|
2. **Настройте уведомления** - чтобы сразу видеть новые логи
|
||||||
|
3. **Создайте отдельного бота для каждого проекта** - не смешивайте логи разных приложений
|
||||||
|
4. **Ограничьте размер логов** - отправляйте только последние 50-100 строк
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово!** Теперь пользователи смогут отправлять анонимные логи, а вы - исправлять баги быстрее! 🚀
|
||||||
187
TELEGRAM_SETUP.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# 📱 Настройка Telegram Бота для Логов
|
||||||
|
|
||||||
|
## 🎯 Цель
|
||||||
|
Пользователи смогут **добровольно** отправлять логи ошибок через Telegram, и вы будете получать их мгновенно в вашу группу/канал.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Шаг 1: Создайте Telegram Бота
|
||||||
|
|
||||||
|
1. Откройте Telegram и найдите **@BotFather**
|
||||||
|
2. Отправьте команду: `/newbot`
|
||||||
|
3. Введите имя бота: **Umbrix Log Bot**
|
||||||
|
4. Введите username: **@umbrix_logs_bot** (или любой свободный)
|
||||||
|
5. **СКОПИРУЙТЕ TOKEN** который выдаст BotFather (будет примерно таким):
|
||||||
|
```
|
||||||
|
1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Шаг 2: Создайте Приватную Группу
|
||||||
|
|
||||||
|
1. Создайте **приватную группу** в Telegram (или канал)
|
||||||
|
2. Назовите её например: **Umbrix Logs**
|
||||||
|
3. **Добавьте бота** в эту группу:
|
||||||
|
- Нажмите на группу → Info → Add members
|
||||||
|
- Найдите вашего бота по username (@umbrix_logs_bot)
|
||||||
|
- Добавьте его
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Шаг 3: Получите CHAT_ID
|
||||||
|
|
||||||
|
### Вариант A: Через веб-запрос
|
||||||
|
|
||||||
|
1. Отправьте **любое сообщение** в созданную группу
|
||||||
|
2. Откройте в браузере (замените `<YOUR_TOKEN>`):
|
||||||
|
```
|
||||||
|
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
|
||||||
|
```
|
||||||
|
Пример:
|
||||||
|
```
|
||||||
|
https://api.telegram.org/bot1234567890:ABCdefGHIjkl/getUpdates
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Найдите в ответе:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"chat": {
|
||||||
|
"id": -1001234567890, ← ЭТО ВАШ CHAT_ID
|
||||||
|
"title": "Umbrix Logs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант B: Через бота @userinfobot
|
||||||
|
|
||||||
|
1. Добавьте **@userinfobot** в вашу группу
|
||||||
|
2. Он автоматически напишет CHAT_ID группы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Шаг 4: Настройка в Коде
|
||||||
|
|
||||||
|
1. **Скопируйте файл-образец**:
|
||||||
|
```bash
|
||||||
|
cd /home/vodorod/dorod/hiddify-original-v2.5.7
|
||||||
|
cp lib/core/telegram_config.dart.example lib/core/telegram_config.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Откройте** `lib/core/telegram_config.dart`
|
||||||
|
|
||||||
|
3. **Вставьте свои данные**:
|
||||||
|
```dart
|
||||||
|
class TelegramConfig {
|
||||||
|
static const String botToken = '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz1234567890';
|
||||||
|
static const String chatId = '-1001234567890'; // С минусом для групп!
|
||||||
|
|
||||||
|
// Остальное не трогайте
|
||||||
|
static const String apiUrl = 'https://api.telegram.org';
|
||||||
|
static const int maxMessageLength = 4000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **ВАЖНО**: Добавьте в `.gitignore`:
|
||||||
|
```bash
|
||||||
|
echo "lib/core/telegram_config.dart" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Шаг 5: Проверка
|
||||||
|
|
||||||
|
1. Соберите приложение:
|
||||||
|
```bash
|
||||||
|
flutter build apk --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Установите на эмулятор/телефон
|
||||||
|
|
||||||
|
3. Зайдите в **Настройки → О приложении**
|
||||||
|
|
||||||
|
4. Нажмите **"Отправить логи разработчику"**
|
||||||
|
|
||||||
|
5. Проверьте вашу Telegram группу - должно прийти сообщение! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### ✅ ЧТО БЕЗОПАСНО:
|
||||||
|
- Токен бота хранится в `telegram_config.dart` (не в Git)
|
||||||
|
- Группа приватная (только вы видите логи)
|
||||||
|
- Логи анонимны (нет IP, email, паролей)
|
||||||
|
|
||||||
|
### ⚠️ ЧТО ОТПРАВЛЯЕТСЯ:
|
||||||
|
- Версия приложения
|
||||||
|
- Версия Android
|
||||||
|
- Модель устройства (Samsung, Xiaomi)
|
||||||
|
- Последние 50 строк логов
|
||||||
|
- Анонимный ID (хэш от device_id)
|
||||||
|
|
||||||
|
### 🚫 ЧТО НЕ ОТПРАВЛЯЕТСЯ:
|
||||||
|
- IP адрес
|
||||||
|
- Email пользователя
|
||||||
|
- VPN конфигурация
|
||||||
|
- Серверы/ключи
|
||||||
|
- История сайтов
|
||||||
|
- Трафик
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Пример Сообщения в Telegram
|
||||||
|
|
||||||
|
```
|
||||||
|
🐛 Отчёт об ошибке Umbrix
|
||||||
|
|
||||||
|
📱 Версия: 1.0.0
|
||||||
|
🤖 Android: 13
|
||||||
|
📲 Устройство: Samsung SM-G991B
|
||||||
|
🆔 ID: a3f7c9d2 (анонимный)
|
||||||
|
🕐 Время: 2024-12-27 15:30:22
|
||||||
|
|
||||||
|
📋 Логи:
|
||||||
|
[ERROR] Connection failed: Timeout
|
||||||
|
[INFO] Retrying connection...
|
||||||
|
[ERROR] Failed after 3 attempts
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Альтернативы
|
||||||
|
|
||||||
|
Если Telegram не подходит, можете использовать:
|
||||||
|
|
||||||
|
1. **Email** (через SMTP или веб-сервис)
|
||||||
|
2. **GitHub Issues** (автоматически через API)
|
||||||
|
3. **Discord Webhook**
|
||||||
|
4. **Slack Webhook**
|
||||||
|
|
||||||
|
Но Telegram - самый простой и удобный вариант для начала! 👍
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
**Q: Бот не отвечает**
|
||||||
|
A: Проверьте что бот добавлен в группу и вы скопировали правильный токен
|
||||||
|
|
||||||
|
**Q: Не могу найти CHAT_ID**
|
||||||
|
A: Убедитесь что отправили сообщение в группу ПОСЛЕ добавления бота
|
||||||
|
|
||||||
|
**Q: Лимит сообщений?**
|
||||||
|
A: Telegram Bot API позволяет 30 сообщений в секунду (более чем достаточно)
|
||||||
|
|
||||||
|
**Q: Можно ли использовать личный чат вместо группы?**
|
||||||
|
A: Да! Напишите боту /start, получите свой chat_id через getUpdates
|
||||||
|
|
||||||
|
**Q: Безопасно ли хранить токен в коде?**
|
||||||
|
A: Да, если файл в .gitignore. Но для production лучше использовать environment variables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата создания**: 27 декабря 2025 г.
|
||||||
116
TEST_APK_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# 📱 Инструкция по тестированию APK на реальном телефоне
|
||||||
|
|
||||||
|
## 1️⃣ Подключи телефон к компьютеру
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включи на телефоне: Настройки → О телефоне → Нажми 7 раз на "Номер сборки"
|
||||||
|
# Затем: Настройки → Для разработчиков → Включи "Отладка по USB"
|
||||||
|
|
||||||
|
# Проверь подключение:
|
||||||
|
adb devices -l
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2️⃣ Удали старую версию (если есть)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb uninstall com.umbrix.app
|
||||||
|
adb uninstall com.hiddify.app.test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3️⃣ Установи новый APK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Универсальный APK (работает на всех телефонах):
|
||||||
|
adb install -r /home/vodorod/dorod/hiddify-original-v2.5.7/build/app/outputs/flutter-apk/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4️⃣ Запусти приложение и собери логи
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Очисти логи:
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# Запусти приложение вручную на телефоне
|
||||||
|
|
||||||
|
# Собери логи (если крашится):
|
||||||
|
adb logcat -d > ~/umbrix_crash.log
|
||||||
|
|
||||||
|
# Или сразу смотри ошибки:
|
||||||
|
adb logcat | grep -E "FATAL|AndroidRuntime|crash|umbrix|Umbrix"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5️⃣ Если появляется ошибка INSTALL_FAILED_NO_MATCHING_ABIS
|
||||||
|
|
||||||
|
Это значит неправильная архитектура. Проверь:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Узнай архитектуру телефона:
|
||||||
|
adb shell getprop ro.product.cpu.abi
|
||||||
|
|
||||||
|
# Если телефон ARM64 (arm64-v8a):
|
||||||
|
adb install -r build/app/outputs/flutter-apk/app-arm64-v8a-debug.apk
|
||||||
|
|
||||||
|
# Если старый ARM (armeabi-v7a):
|
||||||
|
adb install -r build/app/outputs/flutter-apk/app-armeabi-v7a-debug.apk
|
||||||
|
|
||||||
|
# Универсальный (тяжелее, но работает везде):
|
||||||
|
adb install -r build/app/outputs/flutter-apk/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6️⃣ Частые проблемы
|
||||||
|
|
||||||
|
### Краш при запуске:
|
||||||
|
|
||||||
|
**Причина**: Package name изменился, но нативные библиотеки не пересобрались
|
||||||
|
|
||||||
|
**Решение**:
|
||||||
|
```bash
|
||||||
|
cd /home/vodorod/dorod/hiddify-original-v2.5.7
|
||||||
|
flutter clean
|
||||||
|
cd android && ./gradlew clean && cd ..
|
||||||
|
flutter pub get
|
||||||
|
flutter build apk --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка "No matching ABIS":
|
||||||
|
|
||||||
|
**Причина**: APK собран для другой архитектуры (x86 для эмулятора, ARM для телефона)
|
||||||
|
|
||||||
|
**Решение**: Используй `app-debug.apk` (универсальный, 193 MB)
|
||||||
|
|
||||||
|
### Приложение установилось, но не запускается:
|
||||||
|
|
||||||
|
**Решение**: Собери логи:
|
||||||
|
```bash
|
||||||
|
adb logcat -c
|
||||||
|
# Запусти приложение на телефоне
|
||||||
|
adb logcat -d | grep -E "FATAL|Exception" > ~/crash.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📍 Где лежат APK файлы:
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/vodorod/dorod/hiddify-original-v2.5.7/build/app/outputs/flutter-apk/
|
||||||
|
├── app-debug.apk (193 MB) ← ИСПОЛЬЗУЙ ЭТОТ (универсальный)
|
||||||
|
├── app-arm64-v8a-debug.apk (119 MB) ← Для современных телефонов
|
||||||
|
├── app-armeabi-v7a-debug.apk (113 MB) ← Для старых телефонов
|
||||||
|
└── app-x86_64-debug.apk (118 MB) ← Только для эмуляторов
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Как понять почему крашится:
|
||||||
|
|
||||||
|
1. Установи APK
|
||||||
|
2. Запусти приложение
|
||||||
|
3. Собери логи: `adb logcat -d > crash.log`
|
||||||
|
4. Найди строки с `FATAL EXCEPTION` или `AndroidRuntime`
|
||||||
|
5. Покажи мне эти строки
|
||||||
|
|
||||||
|
## ✅ Что изменилось в Umbrix:
|
||||||
|
|
||||||
|
- ✅ Package ID: `com.hiddify.app.test` → `com.umbrix.app`
|
||||||
|
- ✅ App name: "Hiddify" → "Umbrix"
|
||||||
|
- ✅ Icons: новые иконки Umbrix (зелёно-синий щит с черепахой)
|
||||||
|
- ✅ URL scheme: `hiddify://` → `umbrix://`
|
||||||
|
- ✅ Все URL изменены на umbrix.net
|
||||||
|
- ✅ Вкладка "Прокси" → "Локации"
|
||||||
|
- ✅ Privacy policy и Terms созданы
|
||||||
@@ -35,9 +35,9 @@ def flutterVersionCode = localProperties.getProperty('flutter.versionCode')?: '1
|
|||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
|
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.hiddify.hiddify'
|
namespace 'com.umbrix.app'
|
||||||
testNamespace "test.com.hiddify.hiddify"
|
testNamespace "test.com.umbrix.app"
|
||||||
compileSdkVersion 34
|
compileSdkVersion 35
|
||||||
ndkVersion "26.1.10909125"
|
ndkVersion "26.1.10909125"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -54,7 +54,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.hiddify.app.test"
|
applicationId "com.umbrix.app"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
@@ -26,10 +27,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Hiddify"
|
android:label="Umbrix"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -58,11 +57,6 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -74,9 +68,9 @@
|
|||||||
<data android:scheme="clash" />
|
<data android:scheme="clash" />
|
||||||
<data android:host="install-config" />
|
<data android:host="install-config" />
|
||||||
<data android:scheme="clashmeta" />
|
<data android:scheme="clashmeta" />
|
||||||
<data android:scheme="hiddify" />
|
<data android:scheme="umbrix" />
|
||||||
<data android:host="install-sub" />
|
<data android:host="install-sub" />
|
||||||
<data android:scheme="hiddify" />
|
<data android:scheme="umbrix" />
|
||||||
<data android:host="import" />
|
<data android:host="import" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
@@ -85,18 +79,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ShortcutActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/quick_toggle"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:taskAffinity="">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".bg.TileService"
|
android:name=".bg.TileService"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
@@ -131,6 +113,36 @@
|
|||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
android:value="proxy" />
|
android:value="proxy" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- Widgets -->
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.ConnectionWidget1x1"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/widget_description_1x1">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="com.umbrix.app.widget.TOGGLE" />
|
||||||
|
<action android:name="com.umbrix.app.SERVICE_STATE_CHANGED" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info_1x1" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.ConnectionWidget2x2"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/widget_description_2x2">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="com.umbrix.app.widget.TOGGLE" />
|
||||||
|
<action android:name="com.umbrix.app.SERVICE_STATE_CHANGED" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info_2x2" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.hiddify.hiddify;
|
package com.umbrix.app;
|
||||||
|
|
||||||
import com.hiddify.hiddify.IServiceCallback;
|
import com.umbrix.app.IServiceCallback;
|
||||||
|
|
||||||
interface IService {
|
interface IService {
|
||||||
int getStatus();
|
int getStatus();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify;
|
package com.umbrix.app;
|
||||||
|
|
||||||
interface IServiceCallback {
|
interface IServiceCallback {
|
||||||
void onServiceStatusChanged(int status);
|
void onServiceStatusChanged(int status);
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package com.hiddify.hiddify
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Base64
|
|
||||||
import com.hiddify.hiddify.bg.ProxyService
|
|
||||||
import com.hiddify.hiddify.bg.VPNService
|
|
||||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
|
||||||
import com.hiddify.hiddify.constant.ServiceMode
|
|
||||||
import com.hiddify.hiddify.constant.SettingsKey
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.ObjectInputStream
|
|
||||||
|
|
||||||
object Settings {
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
val context = Application.application.applicationContext
|
|
||||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
|
|
||||||
|
|
||||||
var perAppProxyMode: String
|
|
||||||
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
|
|
||||||
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
|
|
||||||
|
|
||||||
val perAppProxyEnabled: Boolean
|
|
||||||
get() = perAppProxyMode != PerAppProxyMode.OFF
|
|
||||||
|
|
||||||
val perAppProxyList: List<String>
|
|
||||||
get() {
|
|
||||||
val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
|
||||||
preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!
|
|
||||||
} else {
|
|
||||||
preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!
|
|
||||||
}
|
|
||||||
if (!stringValue.startsWith(LIST_IDENTIFIER)) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
return decodeListString(stringValue.substring(LIST_IDENTIFIER.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeListString(listString: String): List<String> {
|
|
||||||
val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0)))
|
|
||||||
return stream.readObject() as List<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeConfigPath: String
|
|
||||||
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
|
|
||||||
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
|
|
||||||
|
|
||||||
var activeProfileName: String
|
|
||||||
get() = preferences.getString(SettingsKey.ACTIVE_PROFILE_NAME, "")!!
|
|
||||||
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_PROFILE_NAME, value).apply()
|
|
||||||
|
|
||||||
var serviceMode: String
|
|
||||||
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.VPN)!!
|
|
||||||
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
|
|
||||||
|
|
||||||
var configOptions: String
|
|
||||||
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!!
|
|
||||||
set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply()
|
|
||||||
|
|
||||||
var debugMode: Boolean
|
|
||||||
get() = preferences.getBoolean(SettingsKey.DEBUG_MODE, false)
|
|
||||||
set(value) = preferences.edit().putBoolean(SettingsKey.DEBUG_MODE, value).apply()
|
|
||||||
|
|
||||||
var disableMemoryLimit: Boolean
|
|
||||||
get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
|
|
||||||
set(value) =
|
|
||||||
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
|
|
||||||
|
|
||||||
var dynamicNotification: Boolean
|
|
||||||
get() = preferences.getBoolean(SettingsKey.DYNAMIC_NOTIFICATION, true)
|
|
||||||
set(value) =
|
|
||||||
preferences.edit().putBoolean(SettingsKey.DYNAMIC_NOTIFICATION, value).apply()
|
|
||||||
|
|
||||||
var systemProxyEnabled: Boolean
|
|
||||||
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
|
|
||||||
set(value) =
|
|
||||||
preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
|
|
||||||
|
|
||||||
var startedByUser: Boolean
|
|
||||||
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
|
|
||||||
set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply()
|
|
||||||
|
|
||||||
fun serviceClass(): Class<*> {
|
|
||||||
return when (serviceMode) {
|
|
||||||
ServiceMode.VPN -> VPNService::class.java
|
|
||||||
else -> ProxyService::class.java
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var currentServiceMode : String? = null
|
|
||||||
|
|
||||||
suspend fun rebuildServiceMode(): Boolean {
|
|
||||||
var newMode = ServiceMode.NORMAL
|
|
||||||
try {
|
|
||||||
if (serviceMode == ServiceMode.VPN) {
|
|
||||||
newMode = ServiceMode.VPN
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
if (currentServiceMode == newMode) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
currentServiceMode = newMode
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun needVPNService(): Boolean {
|
|
||||||
val filePath = activeConfigPath
|
|
||||||
if (filePath.isBlank()) return false
|
|
||||||
val content = JSONObject(File(filePath).readText())
|
|
||||||
val inbounds = content.getJSONArray("inbounds")
|
|
||||||
for (index in 0 until inbounds.length()) {
|
|
||||||
val inbound = inbounds.getJSONObject(index)
|
|
||||||
if (inbound.getString("type") == "tun") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.hiddify.hiddify
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ShortcutManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import com.hiddify.hiddify.bg.BoxService
|
|
||||||
import com.hiddify.hiddify.bg.ServiceConnection
|
|
||||||
import com.hiddify.hiddify.constant.Status
|
|
||||||
|
|
||||||
class ShortcutActivity : Activity(), ServiceConnection.Callback {
|
|
||||||
|
|
||||||
private val connection = ServiceConnection(this, this, false)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
|
|
||||||
setResult(
|
|
||||||
RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(
|
|
||||||
this,
|
|
||||||
ShortcutInfoCompat.Builder(this, "toggle")
|
|
||||||
.setIntent(
|
|
||||||
Intent(
|
|
||||||
this,
|
|
||||||
ShortcutActivity::class.java
|
|
||||||
).setAction(Intent.ACTION_MAIN)
|
|
||||||
)
|
|
||||||
.setIcon(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
this,
|
|
||||||
R.mipmap.ic_launcher
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setShortLabel(getString(R.string.quick_toggle))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
connection.connect()
|
|
||||||
if (Build.VERSION.SDK_INT >= 25) {
|
|
||||||
getSystemService<ShortcutManager>()?.reportShortcutUsed("toggle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
moveTaskToBack(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceStatusChanged(status: Status) {
|
|
||||||
when (status) {
|
|
||||||
Status.Started -> BoxService.stop()
|
|
||||||
Status.Stopped -> BoxService.start()
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
connection.disconnect()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.hiddify.hiddify.utils.CommandClient
|
import com.umbrix.app.utils.CommandClient
|
||||||
import com.hiddify.hiddify.utils.ParsedOutboundGroup
|
import com.umbrix.app.utils.ParsedOutboundGroup
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.nekohasekai.libbox.OutboundGroup
|
import io.nekohasekai.libbox.OutboundGroup
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -8,9 +8,9 @@ import android.content.IntentFilter
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import com.hiddify.hiddify.bg.AppChangeReceiver
|
import com.umbrix.app.bg.AppChangeReceiver
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import com.hiddify.hiddify.Application as BoxApplication
|
import com.umbrix.app.Application as BoxApplication
|
||||||
|
|
||||||
class Application : Application() {
|
class Application : Application() {
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.hiddify.hiddify.constant.Alert
|
import com.umbrix.app.constant.Alert
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.JSONMethodCodec
|
import io.flutter.plugin.common.JSONMethodCodec
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.hiddify.hiddify.utils.CommandClient
|
import com.umbrix.app.utils.CommandClient
|
||||||
import com.hiddify.hiddify.utils.ParsedOutboundGroup
|
import com.umbrix.app.utils.ParsedOutboundGroup
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.nekohasekai.libbox.OutboundGroup
|
import io.nekohasekai.libbox.OutboundGroup
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -11,11 +11,11 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.hiddify.hiddify.bg.ServiceConnection
|
import com.umbrix.app.bg.ServiceConnection
|
||||||
import com.hiddify.hiddify.bg.ServiceNotification
|
import com.umbrix.app.bg.ServiceNotification
|
||||||
import com.hiddify.hiddify.constant.Alert
|
import com.umbrix.app.constant.Alert
|
||||||
import com.hiddify.hiddify.constant.ServiceMode
|
import com.umbrix.app.constant.ServiceMode
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.hiddify.hiddify.bg.BoxService
|
import com.umbrix.app.bg.BoxService
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -13,7 +13,7 @@ import android.os.Build
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.hiddify.hiddify.Application.Companion.packageManager
|
import com.umbrix.app.Application.Companion.packageManager
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
@@ -141,7 +141,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
packageManager.getInstalledPackages(flag)
|
packageManager.getInstalledPackages(flag)
|
||||||
}
|
}
|
||||||
val list = mutableListOf<AppItem>()
|
val list = mutableListOf<AppItem>()
|
||||||
installedPackages.forEach {
|
for (it in installedPackages) {
|
||||||
|
val appInfo = it.applicationInfo ?: continue
|
||||||
if (it.packageName != Application.application.packageName &&
|
if (it.packageName != Application.application.packageName &&
|
||||||
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||||
|| it.packageName == "android")
|
|| it.packageName == "android")
|
||||||
@@ -149,8 +150,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
list.add(
|
list.add(
|
||||||
AppItem(
|
AppItem(
|
||||||
it.packageName,
|
it.packageName,
|
||||||
it.applicationInfo.loadLabel(packageManager).toString(),
|
appInfo.loadLabel(packageManager).toString(),
|
||||||
it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ object Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var perAppProxyMode: String
|
var perAppProxyMode: String
|
||||||
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
|
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.EXCLUDE)!!
|
||||||
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
|
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
|
||||||
|
|
||||||
val perAppProxyEnabled: Boolean
|
val perAppProxyEnabled: Boolean
|
||||||
@@ -25,14 +25,59 @@ object Settings {
|
|||||||
|
|
||||||
val perAppProxyList: List<String>
|
val perAppProxyList: List<String>
|
||||||
get() {
|
get() {
|
||||||
val key = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
// Принудительно перечитываем preferences на случай если данные только что изменились
|
||||||
|
val freshPrefs = Application.application.applicationContext
|
||||||
|
.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
android.util.Log.d("Settings", "=== perAppProxyList called ===")
|
||||||
|
val currentMode = freshPrefs.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)
|
||||||
|
android.util.Log.d("Settings", "perAppProxyMode (fresh read) = $currentMode")
|
||||||
|
|
||||||
|
val key = if (currentMode == PerAppProxyMode.INCLUDE) {
|
||||||
SettingsKey.PER_APP_PROXY_INCLUDE_LIST
|
SettingsKey.PER_APP_PROXY_INCLUDE_LIST
|
||||||
} else {
|
} else {
|
||||||
SettingsKey.PER_APP_PROXY_EXCLUDE_LIST
|
SettingsKey.PER_APP_PROXY_EXCLUDE_LIST
|
||||||
}
|
}
|
||||||
// Flutter SharedPreferences plugin сохраняет List<String> как StringSet
|
android.util.Log.d("Settings", "Using key: $key")
|
||||||
// Читаем напрямую без дополнительной сериализации
|
|
||||||
return preferences.getStringSet(key, emptySet())?.toList() ?: emptyList()
|
// Flutter SharedPreferences сохраняет List<String> в специальном формате:
|
||||||
|
// "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" (base64: "This is the prefix for a list.") + JSON
|
||||||
|
val stringValue = freshPrefs.getString(key, "") ?: ""
|
||||||
|
android.util.Log.d("Settings", "Raw value: $stringValue")
|
||||||
|
|
||||||
|
if (stringValue.isEmpty()) {
|
||||||
|
android.util.Log.d("Settings", "Empty value, returning emptyList()")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие префикса Flutter
|
||||||
|
val prefix = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
|
||||||
|
if (!stringValue.startsWith(prefix)) {
|
||||||
|
android.util.Log.e("Settings", "Missing Flutter prefix!")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем префикс и парсим JSON массив
|
||||||
|
val jsonPart = stringValue.substring(prefix.length)
|
||||||
|
android.util.Log.d("Settings", "JSON part: $jsonPart")
|
||||||
|
|
||||||
|
// Flutter добавляет "!" перед JSON массивом, убираем его
|
||||||
|
val cleanJsonPart = if (jsonPart.startsWith("!")) {
|
||||||
|
jsonPart.substring(1)
|
||||||
|
} else {
|
||||||
|
jsonPart
|
||||||
|
}
|
||||||
|
android.util.Log.d("Settings", "Clean JSON: $cleanJsonPart")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val jsonArray = JSONObject("{\"list\":$cleanJsonPart}").getJSONArray("list")
|
||||||
|
val result = List(jsonArray.length()) { jsonArray.getString(it) }
|
||||||
|
android.util.Log.d("Settings", "Parsed list: $result")
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("Settings", "Parse error: ${e.message}", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeConfigPath: String
|
var activeConfigPath: String
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.hiddify.hiddify
|
package com.umbrix.app
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.hiddify.hiddify.utils.CommandClient
|
import com.umbrix.app.utils.CommandClient
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.JSONMethodCodec
|
import io.flutter.plugin.common.JSONMethodCodec
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
|
|
||||||
class AppChangeReceiver : BroadcastReceiver() {
|
class AppChangeReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@@ -13,12 +13,12 @@ import android.util.Log
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.hiddify.hiddify.Application
|
import com.umbrix.app.Application
|
||||||
import com.hiddify.hiddify.R
|
import com.umbrix.app.R
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
import com.hiddify.hiddify.constant.Action
|
import com.umbrix.app.constant.Action
|
||||||
import com.hiddify.hiddify.constant.Alert
|
import com.umbrix.app.constant.Alert
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.nekohasekai.libbox.BoxService
|
import io.nekohasekai.libbox.BoxService
|
||||||
import io.nekohasekai.libbox.CommandServer
|
import io.nekohasekai.libbox.CommandServer
|
||||||
@@ -45,6 +45,14 @@ class BoxService(
|
|||||||
|
|
||||||
private var initializeOnce = false
|
private var initializeOnce = false
|
||||||
private lateinit var workingDir: File
|
private lateinit var workingDir: File
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var currentStatus: Status = Status.Stopped
|
||||||
|
|
||||||
|
fun isConnected(): Boolean {
|
||||||
|
return currentStatus == Status.Started
|
||||||
|
}
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
if (initializeOnce) return
|
if (initializeOnce) return
|
||||||
val baseDir = Application.application.filesDir
|
val baseDir = Application.application.filesDir
|
||||||
@@ -112,6 +120,17 @@ class BoxService(
|
|||||||
private var boxService: BoxService? = null
|
private var boxService: BoxService? = null
|
||||||
private var commandServer: CommandServer? = null
|
private var commandServer: CommandServer? = null
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
|
|
||||||
|
private fun updateStatus(newStatus: Status) {
|
||||||
|
currentStatus = newStatus
|
||||||
|
status.value = newStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postStatus(newStatus: Status) {
|
||||||
|
currentStatus = newStatus
|
||||||
|
status.postValue(newStatus)
|
||||||
|
}
|
||||||
|
|
||||||
private val receiver = object : BroadcastReceiver() {
|
private val receiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
@@ -198,12 +217,15 @@ class BoxService(
|
|||||||
newService.start()
|
newService.start()
|
||||||
boxService = newService
|
boxService = newService
|
||||||
commandServer?.setService(boxService)
|
commandServer?.setService(boxService)
|
||||||
status.postValue(Status.Started)
|
postStatus(Status.Started)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
notification.show(activeProfileName, R.string.status_started)
|
notification.show(activeProfileName, R.string.status_started)
|
||||||
}
|
}
|
||||||
notification.start()
|
notification.start()
|
||||||
|
|
||||||
|
// Уведомляем виджеты о изменении состояния
|
||||||
|
notifyWidgets(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
stopAndAlert(Alert.StartService, e.message)
|
stopAndAlert(Alert.StartService, e.message)
|
||||||
return
|
return
|
||||||
@@ -212,7 +234,7 @@ class BoxService(
|
|||||||
|
|
||||||
override fun serviceReload() {
|
override fun serviceReload() {
|
||||||
notification.close()
|
notification.close()
|
||||||
status.postValue(Status.Starting)
|
postStatus(Status.Starting)
|
||||||
val pfd = fileDescriptor
|
val pfd = fileDescriptor
|
||||||
if (pfd != null) {
|
if (pfd != null) {
|
||||||
pfd.close()
|
pfd.close()
|
||||||
@@ -257,7 +279,7 @@ class BoxService(
|
|||||||
|
|
||||||
private fun stopService() {
|
private fun stopService() {
|
||||||
if (status.value != Status.Started) return
|
if (status.value != Status.Started) return
|
||||||
status.value = Status.Stopping
|
updateStatus(Status.Stopping)
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
service.unregisterReceiver(receiver)
|
service.unregisterReceiver(receiver)
|
||||||
receiverRegistered = false
|
receiverRegistered = false
|
||||||
@@ -289,9 +311,12 @@ class BoxService(
|
|||||||
commandServer = null
|
commandServer = null
|
||||||
Settings.startedByUser = false
|
Settings.startedByUser = false
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
status.value = Status.Stopped
|
updateStatus(Status.Stopped)
|
||||||
service.stopSelf()
|
service.stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Уведомляем виджеты о изменении состояния
|
||||||
|
notifyWidgets(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun postServiceClose() {
|
override fun postServiceClose() {
|
||||||
@@ -309,13 +334,28 @@ class BoxService(
|
|||||||
binder.broadcast { callback ->
|
binder.broadcast { callback ->
|
||||||
callback.onServiceAlert(type.ordinal, message)
|
callback.onServiceAlert(type.ordinal, message)
|
||||||
}
|
}
|
||||||
status.value = Status.Stopped
|
updateStatus(Status.Stopped)
|
||||||
|
|
||||||
|
// Уведомляем виджеты о изменении состояния
|
||||||
|
notifyWidgets(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyWidgets(isConnected: Boolean) {
|
||||||
|
try {
|
||||||
|
val intent = Intent("com.umbrix.app.SERVICE_STATE_CHANGED").apply {
|
||||||
|
putExtra("isConnected", isConnected)
|
||||||
|
setPackage(Application.application.packageName)
|
||||||
|
}
|
||||||
|
Application.application.sendBroadcast(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to notify widgets: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
|
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
|
||||||
status.value = Status.Starting
|
updateStatus(Status.Starting)
|
||||||
|
|
||||||
if (!receiverRegistered) {
|
if (!receiverRegistered) {
|
||||||
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
|
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -8,7 +8,7 @@ import android.net.NetworkRequest
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.hiddify.hiddify.Application
|
import com.umbrix.app.Application
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.hiddify.hiddify.Application
|
import com.umbrix.app.Application
|
||||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||||
|
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.net.DnsResolver
|
import android.net.DnsResolver
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.system.ErrnoException
|
import android.system.ErrnoException
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.hiddify.hiddify.ktx.tryResumeWithException
|
import com.umbrix.app.ktx.tryResumeWithException
|
||||||
import io.nekohasekai.libbox.ExchangeContext
|
import io.nekohasekai.libbox.ExchangeContext
|
||||||
import io.nekohasekai.libbox.LocalDNSTransport
|
import io.nekohasekai.libbox.LocalDNSTransport
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.hiddify.hiddify.Application
|
import com.umbrix.app.Application
|
||||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||||
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
||||||
import io.nekohasekai.libbox.PlatformInterface
|
import io.nekohasekai.libbox.PlatformInterface
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.os.RemoteCallbackList
|
import android.os.RemoteCallbackList
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.hiddify.hiddify.IService
|
import com.umbrix.app.IService
|
||||||
import com.hiddify.hiddify.IServiceCallback
|
import com.umbrix.app.IServiceCallback
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import com.hiddify.hiddify.IService
|
import com.umbrix.app.IService
|
||||||
import com.hiddify.hiddify.IServiceCallback
|
import com.umbrix.app.IServiceCallback
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
import com.hiddify.hiddify.constant.Action
|
import com.umbrix.app.constant.Action
|
||||||
import com.hiddify.hiddify.constant.Alert
|
import com.umbrix.app.constant.Alert
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -13,13 +13,13 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.hiddify.hiddify.Application
|
import com.umbrix.app.Application
|
||||||
import com.hiddify.hiddify.MainActivity
|
import com.umbrix.app.MainActivity
|
||||||
import com.hiddify.hiddify.R
|
import com.umbrix.app.R
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
import com.hiddify.hiddify.constant.Action
|
import com.umbrix.app.constant.Action
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
import com.hiddify.hiddify.utils.CommandClient
|
import com.umbrix.app.utils.CommandClient
|
||||||
import io.nekohasekai.libbox.Libbox
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.libbox.StatusMessage
|
import io.nekohasekai.libbox.StatusMessage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
|
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.hiddify.hiddify.constant.Status
|
import com.umbrix.app.constant.Status
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class TileService : TileService(), ServiceConnection.Callback {
|
class TileService : TileService(), ServiceConnection.Callback {
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.hiddify.hiddify.bg
|
package com.umbrix.app.bg
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
import com.hiddify.hiddify.Settings
|
import com.umbrix.app.Settings
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
import com.umbrix.app.constant.PerAppProxyMode
|
||||||
import com.hiddify.hiddify.ktx.toIpPrefix
|
import com.umbrix.app.ktx.toIpPrefix
|
||||||
import io.nekohasekai.libbox.TunOptions
|
import io.nekohasekai.libbox.TunOptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -19,10 +19,22 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "A/VPNService"
|
private const val TAG = "A/VPNService"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: VPNService? = null
|
||||||
|
|
||||||
|
fun isRunning(): Boolean {
|
||||||
|
return instance != null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val service = BoxService(this, this)
|
private val service = BoxService(this, this)
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
|
||||||
service.onStartCommand(intent, flags, startId)
|
service.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
@@ -35,6 +47,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
instance = null
|
||||||
service.onDestroy()
|
service.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,19 +158,29 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.perAppProxyEnabled) {
|
if (Settings.perAppProxyEnabled) {
|
||||||
|
Log.d(TAG, "=== Per-App Proxy ENABLED ===")
|
||||||
|
Log.d(TAG, "Mode: ${Settings.perAppProxyMode}")
|
||||||
val appList = Settings.perAppProxyList
|
val appList = Settings.perAppProxyList
|
||||||
|
Log.d(TAG, "App list: $appList")
|
||||||
|
|
||||||
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
||||||
|
Log.d(TAG, "Using INCLUDE mode")
|
||||||
appList.forEach {
|
appList.forEach {
|
||||||
|
Log.d(TAG, "Including package: $it")
|
||||||
addIncludePackage(builder,it)
|
addIncludePackage(builder,it)
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "Including self package: $packageName")
|
||||||
addIncludePackage(builder,packageName)
|
addIncludePackage(builder,packageName)
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "Using EXCLUDE mode")
|
||||||
appList.forEach {
|
appList.forEach {
|
||||||
|
Log.d(TAG, "Excluding package: $it")
|
||||||
addExcludePackage(builder,it)
|
addExcludePackage(builder,it)
|
||||||
}
|
}
|
||||||
//addExcludePackage(builder,packageName)
|
//addExcludePackage(builder,packageName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "=== Per-App Proxy DISABLED ===")
|
||||||
val includePackage = options.includePackage
|
val includePackage = options.includePackage
|
||||||
if (includePackage.hasNext()) {
|
if (includePackage.hasNext()) {
|
||||||
while (includePackage.hasNext()) {
|
while (includePackage.hasNext()) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
object Action {
|
object Action {
|
||||||
const val SERVICE = "com.hiddify.app.SERVICE"
|
const val SERVICE = "com.hiddify.app.SERVICE"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
enum class Alert {
|
enum class Alert {
|
||||||
RequestVPNPermission,
|
RequestVPNPermission,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
object PerAppProxyMode {
|
object PerAppProxyMode {
|
||||||
const val OFF = "off"
|
const val OFF = "off"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
object ServiceMode {
|
object ServiceMode {
|
||||||
const val NORMAL = "proxy"
|
const val NORMAL = "proxy"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
object SettingsKey {
|
object SettingsKey {
|
||||||
private const val KEY_PREFIX = "flutter."
|
private const val KEY_PREFIX = "flutter."
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.constant
|
package com.umbrix.app.constant
|
||||||
|
|
||||||
enum class Status {
|
enum class Status {
|
||||||
Stopped,
|
Stopped,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.ktx
|
package com.umbrix.app.ktx
|
||||||
|
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.ktx
|
package com.umbrix.app.ktx
|
||||||
|
|
||||||
import android.net.IpPrefix
|
import android.net.IpPrefix
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.utils
|
package com.umbrix.app.utils
|
||||||
|
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.nekohasekai.libbox.CommandClient
|
import io.nekohasekai.libbox.CommandClient
|
||||||
@@ -9,7 +9,7 @@ import io.nekohasekai.libbox.OutboundGroup
|
|||||||
import io.nekohasekai.libbox.OutboundGroupIterator
|
import io.nekohasekai.libbox.OutboundGroupIterator
|
||||||
import io.nekohasekai.libbox.StatusMessage
|
import io.nekohasekai.libbox.StatusMessage
|
||||||
import io.nekohasekai.libbox.StringIterator
|
import io.nekohasekai.libbox.StringIterator
|
||||||
import com.hiddify.hiddify.ktx.toList
|
import com.umbrix.app.ktx.toList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.hiddify.hiddify.utils
|
package com.umbrix.app.utils
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import io.nekohasekai.libbox.OutboundGroup
|
import io.nekohasekai.libbox.OutboundGroup
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.umbrix.app.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.umbrix.app.R
|
||||||
|
|
||||||
|
class ConnectionWidget1x1 : ConnectionWidgetProvider() {
|
||||||
|
override fun getLayout(): Int = R.layout.widget_connection_1x1
|
||||||
|
|
||||||
|
override fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) {
|
||||||
|
if (isConnected) {
|
||||||
|
views.setImageViewResource(R.id.widget_icon, R.drawable.ic_pause_circle_24)
|
||||||
|
views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_active)
|
||||||
|
} else {
|
||||||
|
views.setImageViewResource(R.id.widget_icon, R.drawable.ic_power_24)
|
||||||
|
views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_inactive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.umbrix.app.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.umbrix.app.R
|
||||||
|
|
||||||
|
class ConnectionWidget2x2 : ConnectionWidgetProvider() {
|
||||||
|
override fun getLayout(): Int = R.layout.widget_connection_2x2
|
||||||
|
|
||||||
|
override fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) {
|
||||||
|
if (isConnected) {
|
||||||
|
views.setImageViewResource(R.id.widget_icon, R.drawable.ic_pause_circle_24)
|
||||||
|
views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_active)
|
||||||
|
} else {
|
||||||
|
views.setImageViewResource(R.id.widget_icon, R.drawable.ic_power_24)
|
||||||
|
views.setInt(R.id.widget_background, "setBackgroundResource", R.drawable.widget_bg_inactive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем текст статуса
|
||||||
|
val statusText = if (isConnected) "Connected" else "Tap to Connect"
|
||||||
|
val statusColor = if (isConnected) Color.parseColor("#00BFA5") else Color.parseColor("#9E9E9E")
|
||||||
|
|
||||||
|
views.setTextViewText(R.id.widget_status, statusText)
|
||||||
|
views.setTextColor(R.id.widget_status, statusColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.umbrix.app.widget
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.umbrix.app.R
|
||||||
|
import com.umbrix.app.bg.BoxService
|
||||||
|
|
||||||
|
abstract class ConnectionWidgetProvider : AppWidgetProvider() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_TOGGLE = "com.umbrix.app.widget.TOGGLE"
|
||||||
|
|
||||||
|
fun updateAllWidgets(context: Context, isConnected: Boolean) {
|
||||||
|
// Обновляем все виджеты 1x1
|
||||||
|
updateWidgets(context, ConnectionWidget1x1::class.java, isConnected)
|
||||||
|
// Обновляем все виджеты 2x2
|
||||||
|
updateWidgets(context, ConnectionWidget2x2::class.java, isConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWidgets(context: Context, widgetClass: Class<out ConnectionWidgetProvider>, isConnected: Boolean) {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, widgetClass)
|
||||||
|
val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
|
||||||
|
appWidgetIds.forEach { appWidgetId ->
|
||||||
|
val instance = widgetClass.getDeclaredConstructor().newInstance()
|
||||||
|
instance.updateAppWidget(context, appWidgetManager, appWidgetId, isConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun getLayout(): Int
|
||||||
|
|
||||||
|
protected open fun updateWidgetUI(context: Context, views: RemoteViews, isConnected: Boolean) {
|
||||||
|
// Переопределяется в подклассах
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun updateAppWidget(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetId: Int,
|
||||||
|
isConnected: Boolean
|
||||||
|
) {
|
||||||
|
val views = RemoteViews(context.packageName, getLayout())
|
||||||
|
|
||||||
|
// Обновляем UI
|
||||||
|
updateWidgetUI(context, views, isConnected)
|
||||||
|
|
||||||
|
// Настраиваем клик на весь виджет
|
||||||
|
val toggleIntent = Intent(context, this::class.java).apply {
|
||||||
|
action = ACTION_TOGGLE
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
}
|
||||||
|
val togglePendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
appWidgetId,
|
||||||
|
toggleIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Устанавливаем клик на фон виджета
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_background, togglePendingIntent)
|
||||||
|
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdate(
|
||||||
|
context: Context,
|
||||||
|
appWidgetManager: AppWidgetManager,
|
||||||
|
appWidgetIds: IntArray
|
||||||
|
) {
|
||||||
|
val isConnected = com.umbrix.app.bg.BoxService.isConnected()
|
||||||
|
appWidgetIds.forEach { appWidgetId ->
|
||||||
|
updateAppWidget(context, appWidgetManager, appWidgetId, isConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_TOGGLE -> {
|
||||||
|
if (BoxService.isConnected()) {
|
||||||
|
BoxService.stop()
|
||||||
|
} else {
|
||||||
|
BoxService.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"com.umbrix.app.SERVICE_STATE_CHANGED" -> {
|
||||||
|
val isConnected = intent.getBooleanExtra("isConnected", false)
|
||||||
|
updateAllWidgets(context, isConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 5.5 KiB |
@@ -3,7 +3,5 @@
|
|||||||
<item>
|
<item>
|
||||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<!-- Убрали логотип, будет показан Flutter виджет с круговым индикатором -->
|
||||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
@@ -1,29 +1,14 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="2048"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="2048">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.85"
|
<group
|
||||||
android:scaleY="0.85"
|
android:translateX="20"
|
||||||
android:translateX="153.6"
|
android:translateY="20">
|
||||||
android:translateY="153.6">
|
<path
|
||||||
<group>
|
android:pathData="M20,10 L20,45 Q20,60 35,60 L35,60 Q50,60 50,45 L50,10 L42,10 L42,45 Q42,52 35,52 L35,52 Q28,52 28,45 L28,10 Z"
|
||||||
<clip-path
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M645,608h795v795h-795z"/>
|
android:strokeWidth="0"/>
|
||||||
<path
|
|
||||||
android:pathData="M1230.8,787.2C1230.8,779.9 1234.6,773.1 1240.9,769.3L1408.3,668.9C1422.3,660.5 1440,670.5 1440,686.8V859C1440,870.6 1430.6,880 1419.1,880H1251.7C1240.2,880 1230.8,870.6 1230.8,859V787.2Z"
|
|
||||||
android:fillColor="#455FE9"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M937.9,954.6C937.9,947.3 941.8,940.4 948.1,936.7L1115.4,836.2C1129.4,827.9 1147.1,837.9 1147.1,854.2V1152V1172.9V1361.2C1147.1,1372.7 1137.7,1382.1 1126.2,1382.1H958.8C947.3,1382.1 937.9,1372.7 937.9,1361.2V1172.9V1152V954.6ZM655.2,1124.9C648.9,1128.7 645,1135.6 645,1142.9V1361.2C645,1372.7 654.4,1382.1 665.9,1382.1H833.3C844.8,1382.1 854.2,1372.7 854.2,1361.2V1042.5C854.2,1026.2 836.5,1016.2 822.5,1024.5L655.2,1124.9Z"
|
|
||||||
android:fillColor="#455FE9"
|
|
||||||
android:fillType="evenOdd"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M854.2,1172.9H728.7V1340.3H854.2V1298.4C854.2,1286.9 863.6,1277.5 875.1,1277.5H917C928.5,1277.5 937.9,1286.9 937.9,1298.4V1340.3H1105.3V1172.9H937.9V1214.7C937.9,1226.3 928.5,1235.6 917,1235.6H875.1C863.6,1235.6 854.2,1226.3 854.2,1214.7V1172.9Z"
|
|
||||||
android:fillColor="#455FE9"
|
|
||||||
android:fillType="evenOdd"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M1230.8,942.8V1361.2C1230.8,1372.7 1240.2,1382.1 1251.7,1382.1H1419.1C1430.6,1382.1 1440,1372.7 1440,1361.2V942.8C1440,931.2 1430.6,921.8 1419.1,921.8H1251.7C1240.2,921.8 1230.8,931.2 1230.8,942.8Z"
|
|
||||||
android:fillColor="#455FE9"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
5
android/app/src/main/res/drawable/ic_pause_circle_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector android:height="24.0dip" android:width="24.0dip" android:viewportWidth="24.0" android:viewportHeight="24.0"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#ff00ed44" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM10,16L10,16c-0.55,0 -1,-0.45 -1,-1V9c0,-0.55 0.45,-1 1,-1l0,0c0.55,0 1,0.45 1,1v6C11,15.55 10.55,16 10,16zM14,16L14,16c-0.55,0 -1,-0.45 -1,-1V9c0,-0.55 0.45,-1 1,-1l0,0c0.55,0 1,0.45 1,1v6C15,15.55 14.55,16 14,16z" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_play_widget.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M8,5v14l11,-7z"/>
|
||||||
|
</vector>
|
||||||
5
android/app/src/main/res/drawable/ic_power_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector android:height="24.0dip" android:width="24.0dip" android:viewportWidth="24.0" android:viewportHeight="24.0"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#ff007aff" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2z M11.25,5.8h1.5v6.2h-1.5z M15.949,8.057l-0.874,0.874c1.693,1.693 1.686,4.439 -0.006,6.132c-1.693,1.693 -4.445,1.693 -6.132,0.006c-1.693,-1.693 -1.698,-4.452 -0.006,-6.144L8.057,8.057c-2.176,2.176 -2.176,5.71 0.006,7.893c2.176,2.176 5.71,2.176 7.886,-0.006C18.126,13.767 18.126,10.239 15.949,8.057z" android:fillType="evenOdd" />
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_power_widget.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M13,3h-2v10h2V3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9C21,9.26 19.77,6.82 17.83,5.17z"/>
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_stop_widget.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M6,6h12v12H6z"/>
|
||||||
|
</vector>
|
||||||
@@ -3,7 +3,5 @@
|
|||||||
<item>
|
<item>
|
||||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<!-- Убрали логотип, будет показан Flutter виджет с круговым индикатором -->
|
||||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
17
android/app/src/main/res/drawable/widget_bg_active.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Connected state: Teal color with stroke -->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<!-- Teal - solid and vibrant -->
|
||||||
|
<solid android:color="#00BFA5" />
|
||||||
|
|
||||||
|
<!-- Glow effect with lighter stroke -->
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#7ED6C6" />
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="60dp"
|
||||||
|
android:height="60dp" />
|
||||||
|
</shape>
|
||||||
17
android/app/src/main/res/drawable/widget_bg_inactive.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Disconnected state: Gray circle with subtle elevation -->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<!-- Gray with 80% opacity for disconnected state -->
|
||||||
|
<solid android:color="#CC9E9E9E" />
|
||||||
|
|
||||||
|
<!-- Subtle stroke for definition -->
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#40000000" />
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="60dp"
|
||||||
|
android:height="60dp" />
|
||||||
|
</shape>
|
||||||
32
android/app/src/main/res/drawable/widget_preview_1x1.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Фон -->
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#CC9E9E9E" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#40000000" />
|
||||||
|
<size
|
||||||
|
android:width="60dp"
|
||||||
|
android:height="60dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- Иконка power по центру -->
|
||||||
|
<item
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:gravity="center">
|
||||||
|
<vector
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ff007aff"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2z M11.25,5.8h1.5v6.2h-1.5z M15.949,8.057l-0.874,0.874c1.693,1.693 1.686,4.439 -0.006,6.132c-1.693,1.693 -4.445,1.693 -6.132,0.006c-1.693,-1.693 -1.698,-4.452 -0.006,-6.144L8.057,8.057c-2.176,2.176 -2.176,5.71 0.006,7.893c2.176,2.176 5.71,2.176 7.886,-0.006C18.126,13.767 18.126,10.239 15.949,8.057z"
|
||||||
|
android:fillType="evenOdd" />
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
39
android/app/src/main/res/drawable/widget_preview_2x2.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Фон -->
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- Круг в центре -->
|
||||||
|
<item
|
||||||
|
android:width="60dp"
|
||||||
|
android:height="60dp"
|
||||||
|
android:gravity="center">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#CC9E9E9E" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#40000000" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<!-- Иконка power по центру -->
|
||||||
|
<item
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:gravity="center">
|
||||||
|
<vector
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ff007aff"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2z M11.25,5.8h1.5v6.2h-1.5z M15.949,8.057l-0.874,0.874c1.693,1.693 1.686,4.439 -0.006,6.132c-1.693,1.693 -4.445,1.693 -6.132,0.006c-1.693,-1.693 -1.698,-4.452 -0.006,-6.144L8.057,8.057c-2.176,2.176 -2.176,5.71 0.006,7.893c2.176,2.176 5.71,2.176 7.886,-0.006C18.126,13.767 18.126,10.239 15.949,8.057z"
|
||||||
|
android:fillType="evenOdd" />
|
||||||
|
</vector>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
17
android/app/src/main/res/layout/widget_connection_1x1.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/widget_background"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_icon"
|
||||||
|
android:layout_width="45dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:srcCompat="@drawable/ic_power_24" />
|
||||||
|
</LinearLayout>
|
||||||
34
android/app/src/main/res/layout/widget_connection_2x2.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/widget_background"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_icon"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:padding="22dp"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Tap to Connect"
|
||||||
|
android:textColor="#AAAAAA"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_banner_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 824 B |
|
Before Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 646 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#F0F3FA</color>
|
<color name="ic_launcher_background">#2D3748</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="app_name">Umbrix</string>
|
||||||
<string name="stop">Stop</string>
|
<string name="stop">Stop</string>
|
||||||
<string name="quick_toggle">Toggle</string>
|
|
||||||
<string name="status_starting">Service starting…</string>
|
<string name="status_starting">Service starting…</string>
|
||||||
<string name="status_started">Service started</string>
|
<string name="status_started">Service started</string>
|
||||||
|
<string name="widget_description_1x1">Connection Button</string>
|
||||||
|
<string name="widget_description_2x2">Connection Widget</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,13 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<shortcut
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:shortcutId="toggle"
|
|
||||||
android:shortcutLongLabel="@string/quick_toggle"
|
|
||||||
android:shortcutShortLabel="@string/quick_toggle">
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.MAIN"
|
|
||||||
android:targetClass="com.hiddify.hiddify.ShortcutActivity"
|
|
||||||
android:targetPackage="app.hiddify.com" />
|
|
||||||
</shortcut>
|
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
13
android/app/src/main/res/xml/widget_info_1x1.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="40dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:targetCellWidth="1"
|
||||||
|
android:targetCellHeight="1"
|
||||||
|
android:updatePeriodMillis="0"
|
||||||
|
android:initialLayout="@layout/widget_connection_1x1"
|
||||||
|
android:description="@string/widget_description_1x1"
|
||||||
|
android:previewImage="@drawable/widget_preview_1x1"
|
||||||
|
android:resizeMode="none"
|
||||||
|
android:widgetCategory="home_screen">
|
||||||
|
</appwidget-provider>
|
||||||
15
android/app/src/main/res/xml/widget_info_2x2.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:targetCellWidth="2"
|
||||||
|
android:targetCellHeight="2"
|
||||||
|
android:maxResizeWidth="250dp"
|
||||||
|
android:maxResizeHeight="250dp"
|
||||||
|
android:updatePeriodMillis="0"
|
||||||
|
android:initialLayout="@layout/widget_connection_2x2"
|
||||||
|
android:description="@string/widget_description_2x2"
|
||||||
|
android:previewImage="@drawable/widget_preview_2x2"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen">
|
||||||
|
</appwidget-provider>
|
||||||
33
appcast.xml
@@ -1,34 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Release</title>
|
<title>Umbrix Updates</title>
|
||||||
<item>
|
<!-- No updates available - this is Umbrix, not Hiddify -->
|
||||||
<title>Version 0.13.6</title>
|
|
||||||
<pubDate>Sun, 7 Jan 2024 22:00:00 +0000</pubDate>
|
|
||||||
<enclosure
|
|
||||||
url="https://play.google.com/store/apps/details?id=app.hiddify.com"
|
|
||||||
sparkle:version="0.13.6" sparkle:os="android" />
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>Version 0.13.6</title>
|
|
||||||
<pubDate>Sun, 7 Jan 2024 22:00:00 +0000</pubDate>
|
|
||||||
<enclosure
|
|
||||||
url="https://github.com/hiddify/hiddify-next/releases/download/v0.13.6/hiddify-windows-x64-setup.zip"
|
|
||||||
sparkle:version="0.13.6" sparkle:os="windows" />
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>Version 0.13.6</title>
|
|
||||||
<pubDate>Sun, 7 Jan 2024 22:00:00 +0000</pubDate>
|
|
||||||
<enclosure
|
|
||||||
url="https://github.com/hiddify/hiddify-next/releases/download/v0.13.6/hiddify-macos-universal.zip"
|
|
||||||
sparkle:version="0.13.6" sparkle:os="macos" />
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>Version 0.13.6</title>
|
|
||||||
<pubDate>Sun, 7 Jan 2024 22:00:00 +0000</pubDate>
|
|
||||||
<enclosure
|
|
||||||
url="https://github.com/hiddify/hiddify-next/releases/download/v0.13.6/hiddify-linux-x64.zip"
|
|
||||||
sparkle:version="0.13.6" sparkle:os="linux" />
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
|
|||||||