Backup before removing hiddify references

This commit is contained in:
Hiddify User
2026-01-15 12:28:40 +03:00
parent f54603d129
commit 36d9e31236
231 changed files with 6648 additions and 1832 deletions

1
.fvm/flutter_sdk Symbolic link
View File

@@ -0,0 +1 @@
/home/vodorod/fvm/versions/3.24.0

3
.fvm/fvm_config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"flutterSdkVersion": "3.24.0"
}

1
.fvm/release Normal file
View File

@@ -0,0 +1 @@
3.24.0

0
.fvm/version Normal file
View File

1
.fvm/versions/3.24.0 Symbolic link
View File

@@ -0,0 +1 @@
/home/vodorod/fvm/versions/3.24.0

2
.gitignore vendored
View File

@@ -59,4 +59,4 @@ app.*.map.json
/data
# FVM Version Cache
.fvm/
.fvm/lib/core/telegram_config.dart

306
AUDIT_REPORT.md Normal file
View 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)

View 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
View 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
View 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
View 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
View 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 созданы

View File

@@ -35,9 +35,9 @@ def flutterVersionCode = localProperties.getProperty('flutter.versionCode')?: '1
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
android {
namespace 'com.hiddify.hiddify'
testNamespace "test.com.hiddify.hiddify"
compileSdkVersion 34
namespace 'com.umbrix.app'
testNamespace "test.com.umbrix.app"
compileSdkVersion 35
ndkVersion "26.1.10909125"
compileOptions {
@@ -54,7 +54,7 @@ android {
}
defaultConfig {
applicationId "com.hiddify.app.test"
applicationId "com.umbrix.app"
minSdkVersion 21
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()

View File

@@ -12,6 +12,7 @@
android:required="false" />
<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_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -26,10 +27,8 @@
<application
android:name=".Application"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="Hiddify"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="Umbrix"
tools:targetApi="31">
<meta-data
@@ -58,11 +57,6 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -74,9 +68,9 @@
<data android:scheme="clash" />
<data android:host="install-config" />
<data android:scheme="clashmeta" />
<data android:scheme="hiddify" />
<data android:scheme="umbrix" />
<data android:host="install-sub" />
<data android:scheme="hiddify" />
<data android:scheme="umbrix" />
<data android:host="import" />
</intent-filter>
@@ -85,18 +79,6 @@
</intent-filter>
</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
android:name=".bg.TileService"
android:directBootAware="true"
@@ -131,6 +113,36 @@
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</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.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@@ -1,6 +1,6 @@
package com.hiddify.hiddify;
package com.umbrix.app;
import com.hiddify.hiddify.IServiceCallback;
import com.umbrix.app.IServiceCallback;
interface IService {
int getStatus();

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify;
package com.umbrix.app;
interface IServiceCallback {
void onServiceStatusChanged(int status);

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.util.Log
import com.google.gson.Gson
import com.hiddify.hiddify.utils.CommandClient
import com.hiddify.hiddify.utils.ParsedOutboundGroup
import com.umbrix.app.utils.CommandClient
import com.umbrix.app.utils.ParsedOutboundGroup
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.nekohasekai.libbox.OutboundGroup

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.app.Application
import android.app.NotificationManager
@@ -8,9 +8,9 @@ import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.hiddify.hiddify.bg.AppChangeReceiver
import com.umbrix.app.bg.AppChangeReceiver
import go.Seq
import com.hiddify.hiddify.Application as BoxApplication
import com.umbrix.app.Application as BoxApplication
class Application : Application() {

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.util.Log
import androidx.lifecycle.Observer
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.constant.Alert
import com.umbrix.app.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.JSONMethodCodec

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.util.Log
import com.google.gson.Gson
import com.hiddify.hiddify.utils.CommandClient
import com.hiddify.hiddify.utils.ParsedOutboundGroup
import com.umbrix.app.utils.CommandClient
import com.umbrix.app.utils.ParsedOutboundGroup
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.nekohasekai.libbox.OutboundGroup

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.annotation.SuppressLint
import android.content.Intent
@@ -11,11 +11,11 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.hiddify.hiddify.bg.ServiceConnection
import com.hiddify.hiddify.bg.ServiceNotification
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.ServiceMode
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.bg.ServiceConnection
import com.umbrix.app.bg.ServiceNotification
import com.umbrix.app.constant.Alert
import com.umbrix.app.constant.ServiceMode
import com.umbrix.app.constant.Status
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import kotlinx.coroutines.Dispatchers

View File

@@ -1,8 +1,8 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.util.Log
import com.hiddify.hiddify.bg.BoxService
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.bg.BoxService
import com.umbrix.app.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify
package com.umbrix.app
import android.Manifest
import android.annotation.SuppressLint
@@ -13,7 +13,7 @@ import android.os.Build
import android.util.Base64
import com.google.gson.Gson
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.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -141,7 +141,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
packageManager.getInstalledPackages(flag)
}
val list = mutableListOf<AppItem>()
installedPackages.forEach {
for (it in installedPackages) {
val appInfo = it.applicationInfo ?: continue
if (it.packageName != Application.application.packageName &&
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android")
@@ -149,8 +150,8 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
list.add(
AppItem(
it.packageName,
it.applicationInfo.loadLabel(packageManager).toString(),
it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
appInfo.loadLabel(packageManager).toString(),
appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
)
)
}

View File

@@ -17,7 +17,7 @@ object Settings {
}
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()
val perAppProxyEnabled: Boolean
@@ -25,14 +25,59 @@ object Settings {
val perAppProxyList: List<String>
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
} else {
SettingsKey.PER_APP_PROXY_EXCLUDE_LIST
}
// Flutter SharedPreferences plugin сохраняет List<String> как StringSet
// Читаем напрямую без дополнительной сериализации
return preferences.getStringSet(key, emptySet())?.toList() ?: emptyList()
android.util.Log.d("Settings", "Using key: $key")
// 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

View File

@@ -1,7 +1,7 @@
package com.hiddify.hiddify
package com.umbrix.app
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.plugin.common.EventChannel
import io.flutter.plugin.common.JSONMethodCodec

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.hiddify.hiddify.Settings
import com.umbrix.app.Settings
class AppChangeReceiver : BroadcastReceiver() {

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.hiddify.hiddify.Settings
import com.umbrix.app.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.app.Service
import android.content.BroadcastReceiver
@@ -13,12 +13,12 @@ import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.Application
import com.umbrix.app.R
import com.umbrix.app.Settings
import com.umbrix.app.constant.Action
import com.umbrix.app.constant.Alert
import com.umbrix.app.constant.Status
import go.Seq
import io.nekohasekai.libbox.BoxService
import io.nekohasekai.libbox.CommandServer
@@ -45,6 +45,14 @@ class BoxService(
private var initializeOnce = false
private lateinit var workingDir: File
@Volatile
private var currentStatus: Status = Status.Stopped
fun isConnected(): Boolean {
return currentStatus == Status.Started
}
private fun initialize() {
if (initializeOnce) return
val baseDir = Application.application.filesDir
@@ -112,6 +120,17 @@ class BoxService(
private var boxService: BoxService? = null
private var commandServer: CommandServer? = null
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() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
@@ -198,12 +217,15 @@ class BoxService(
newService.start()
boxService = newService
commandServer?.setService(boxService)
status.postValue(Status.Started)
postStatus(Status.Started)
withContext(Dispatchers.Main) {
notification.show(activeProfileName, R.string.status_started)
}
notification.start()
// Уведомляем виджеты о изменении состояния
notifyWidgets(true)
} catch (e: Exception) {
stopAndAlert(Alert.StartService, e.message)
return
@@ -212,7 +234,7 @@ class BoxService(
override fun serviceReload() {
notification.close()
status.postValue(Status.Starting)
postStatus(Status.Starting)
val pfd = fileDescriptor
if (pfd != null) {
pfd.close()
@@ -257,7 +279,7 @@ class BoxService(
private fun stopService() {
if (status.value != Status.Started) return
status.value = Status.Stopping
updateStatus(Status.Stopping)
if (receiverRegistered) {
service.unregisterReceiver(receiver)
receiverRegistered = false
@@ -289,9 +311,12 @@ class BoxService(
commandServer = null
Settings.startedByUser = false
withContext(Dispatchers.Main) {
status.value = Status.Stopped
updateStatus(Status.Stopped)
service.stopSelf()
}
// Уведомляем виджеты о изменении состояния
notifyWidgets(false)
}
}
override fun postServiceClose() {
@@ -309,13 +334,28 @@ class BoxService(
binder.broadcast { callback ->
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 {
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
status.value = Status.Starting
updateStatus(Status.Starting)
if (!receiverRegistered) {
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.annotation.TargetApi
import android.net.ConnectivityManager
@@ -8,7 +8,7 @@ import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.hiddify.hiddify.Application
import com.umbrix.app.Application
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope

View File

@@ -1,8 +1,8 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.net.Network
import android.os.Build
import com.hiddify.hiddify.Application
import com.umbrix.app.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
import java.net.NetworkInterface

View File

@@ -1,11 +1,11 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.net.DnsResolver
import android.os.Build
import android.os.CancellationSignal
import android.system.ErrnoException
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.LocalDNSTransport
import kotlinx.coroutines.Dispatchers

View File

@@ -1,10 +1,10 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.Application
import com.umbrix.app.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.app.Service
import android.content.Intent

View File

@@ -1,10 +1,10 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.os.RemoteCallbackList
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.IService
import com.umbrix.app.IServiceCallback
import com.umbrix.app.constant.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

View File

@@ -1,11 +1,11 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.IService
import com.umbrix.app.IServiceCallback
import com.umbrix.app.Settings
import com.umbrix.app.constant.Action
import com.umbrix.app.constant.Alert
import com.umbrix.app.constant.Status
import android.content.ComponentName
import android.content.Context
import android.content.Intent

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -13,13 +13,13 @@ import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.MainActivity
import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Status
import com.hiddify.hiddify.utils.CommandClient
import com.umbrix.app.Application
import com.umbrix.app.MainActivity
import com.umbrix.app.R
import com.umbrix.app.Settings
import com.umbrix.app.constant.Action
import com.umbrix.app.constant.Status
import com.umbrix.app.utils.CommandClient
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.StatusMessage
import kotlinx.coroutines.Dispatchers

View File

@@ -1,9 +1,9 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.constant.Status
import com.umbrix.app.constant.Status
@RequiresApi(24)
class TileService : TileService(), ServiceConnection.Callback {

View File

@@ -1,15 +1,15 @@
package com.hiddify.hiddify.bg
package com.umbrix.app.bg
import android.util.Log
import com.hiddify.hiddify.Settings
import com.umbrix.app.Settings
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import com.hiddify.hiddify.constant.PerAppProxyMode
import com.hiddify.hiddify.ktx.toIpPrefix
import com.umbrix.app.constant.PerAppProxyMode
import com.umbrix.app.ktx.toIpPrefix
import io.nekohasekai.libbox.TunOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -19,10 +19,22 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
companion object {
private const val TAG = "A/VPNService"
@Volatile
private var instance: VPNService? = null
fun isRunning(): Boolean {
return instance != null
}
}
private val service = BoxService(this, this)
override fun onCreate() {
super.onCreate()
instance = this
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
service.onStartCommand(intent, flags, startId)
@@ -35,6 +47,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
}
override fun onDestroy() {
instance = null
service.onDestroy()
}
@@ -145,19 +158,29 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
}
if (Settings.perAppProxyEnabled) {
Log.d(TAG, "=== Per-App Proxy ENABLED ===")
Log.d(TAG, "Mode: ${Settings.perAppProxyMode}")
val appList = Settings.perAppProxyList
Log.d(TAG, "App list: $appList")
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
Log.d(TAG, "Using INCLUDE mode")
appList.forEach {
Log.d(TAG, "Including package: $it")
addIncludePackage(builder,it)
}
Log.d(TAG, "Including self package: $packageName")
addIncludePackage(builder,packageName)
} else {
Log.d(TAG, "Using EXCLUDE mode")
appList.forEach {
Log.d(TAG, "Excluding package: $it")
addExcludePackage(builder,it)
}
//addExcludePackage(builder,packageName)
}
} else {
Log.d(TAG, "=== Per-App Proxy DISABLED ===")
val includePackage = options.includePackage
if (includePackage.hasNext()) {
while (includePackage.hasNext()) {

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
object Action {
const val SERVICE = "com.hiddify.app.SERVICE"

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
enum class Alert {
RequestVPNPermission,

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
object PerAppProxyMode {
const val OFF = "off"

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
object ServiceMode {
const val NORMAL = "proxy"

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
object SettingsKey {
private const val KEY_PREFIX = "flutter."

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.constant
package com.umbrix.app.constant
enum class Status {
Stopped,

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.ktx
package com.umbrix.app.ktx
import kotlin.coroutines.Continuation

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.ktx
package com.umbrix.app.ktx
import android.net.IpPrefix
import android.os.Build

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.utils
package com.umbrix.app.utils
import go.Seq
import io.nekohasekai.libbox.CommandClient
@@ -9,7 +9,7 @@ import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.libbox.OutboundGroupIterator
import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.libbox.StringIterator
import com.hiddify.hiddify.ktx.toList
import com.umbrix.app.ktx.toList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay

View File

@@ -1,4 +1,4 @@
package com.hiddify.hiddify.utils
package com.umbrix.app.utils
import com.google.gson.annotations.SerializedName
import io.nekohasekai.libbox.OutboundGroup

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -3,7 +3,5 @@
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
<!-- Убрали логотип, будет показан Flutter виджет с круговым индикатором -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -1,29 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="2048"
android:viewportHeight="2048">
<group android:scaleX="0.85"
android:scaleY="0.85"
android:translateX="153.6"
android:translateY="153.6">
<group>
<clip-path
android:pathData="M645,608h795v795h-795z"/>
<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>
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:translateX="20"
android:translateY="20">
<path
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"
android:fillColor="#FFFFFF"
android:strokeWidth="0"/>
</group>
</vector>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -3,7 +3,5 @@
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
<!-- Убрали логотип, будет показан Flutter виджет с круговым индикатором -->
</layer-list>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F0F3FA</color>
<color name="ic_launcher_background">#2D3748</color>
</resources>

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Umbrix</string>
<string name="stop">Stop</string>
<string name="quick_toggle">Toggle</string>
<string name="status_starting">Service starting…</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>

View File

@@ -1,13 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View 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>

View 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>

View File

@@ -1,34 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Release</title>
<item>
<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>
<title>Umbrix Updates</title>
<!-- No updates available - this is Umbrix, not Hiddify -->
</channel>
</rss>

Some files were not shown because too many files have changed in this diff Show More