从 /Users/yunyanzhineng/work/eaimar_client-main 搬入,未带原 .git,历史留在原仓库。 3215 个文件 / 约 118MB,其中约 40MB 是构建必需的预编译 SDK(azure_speech 的 MicrosoftCognitiveServicesSpeech.xcframework、device_jieli 的 JL_BLEKit 等)。 build/ (5.2G)、ios/Pods/ (311M)、android/.gradle/ (120M) 已由 .gitignore 挡掉。 本次入库的代码包含两项刚做完的改动: 1) 游客登录入口接上服务端开关。此前 _checkGuestLoginConfig() 恒定写死 true, 入口根本不读配置。现改为取服务端算好的结论,并把入口所在的 Builder 改成 Obx ——配置是异步拿到的,Builder 只渲染 onInit 时的初值 false,入口再也不会出现。 拿不到配置时按隐藏处理:默认显示会在送审期把不该露的入口露给审核员。 ⚠️ 已发布的老客户端关不掉(写死 true),只对本版及以后生效。 2) iOS 上架合规修复: - 移除 ATT/IDFA:删掉 Info.plist 的 NSUserTrackingUsageDescription、 SKAdNetworkItems、NSAdvertisingAttributionReportEndpoint,删掉 splash 里的 授权请求与 app_tracking_transparency 依赖。本 App 从不读取 IDFA,也没有任何 广告/归因 SDK,弹框纯粹劝退用户,还要在 App Store Connect 申报追踪数据类型。 - ios/Podfile 里那段 config.build_settings.delete('NSUserTrackingUsageDescription') 是无效代码(该 key 在 Info.plist 里,不是 build setting),换成注释说明。 - 通讯录符号(ITMS-90683 被拒原因)随之一并解决:permission_handler 走 SPM 后 按 Info.plist 有无对应 key 决定编不编,判定结果被 SwiftPM 按内容哈希缓存, 改完 plist 必须清 DerivedData 与 manifests 缓存才会重新求值。 另附 GOOGLE_PLAY_RELEASE_CHECKLIST.md:Google Play 上架阻塞项的实测梳理 (targetSdk 36 与 Billing 8 的 2026-08-31 期限、16KB 页对齐不合规的两个 .so、 支付回退逻辑的下架风险)。 ⚠️ android/app/sign/ 下的 eaimar.keystore 与 Eaimar.properties(含明文签名密码) 随本次提交入库,与本仓库"私有仓库下接受真实凭据"的既有取舍一致。 仓库若要公开,这两个必须先摘出去。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>main
@ -0,0 +1,74 @@ |
|||||
|
# ============================================================ |
||||
|
# apps/client 本机运行时配置(flutter_dotenv) |
||||
|
# |
||||
|
# cp .env.example .env 然后按需填值。 |
||||
|
# |
||||
|
# ⚠️ .env 同时被 pubspec.yaml 声明为 asset,且 main.dart 里 `dotenv.load(fileName: ".env")` |
||||
|
# 是启动第一步 —— 文件不存在 App 直接起不来,不是可选项。 |
||||
|
# ⚠️ .env 已在 .gitignore 中(源仓库也一样忽略),别提交。 |
||||
|
# |
||||
|
# 注意区分两套 "env": |
||||
|
# - 本文件 = 本机/渠道级配置,编译进包,用 dotenv.env['X'] 读; |
||||
|
# - AppConfig.env('X') = 后端 user_getappconfig 下发的配置,由 console 后台维护。 |
||||
|
# 下面那一堆 VOLCANO_* / OPENAI_* 在当前线上 .env 里**实际都是空的** —— 第三方能力已改由 |
||||
|
# 后端下发,本文件只在需要绕过后端、直连调试时才填。真正必须填的只有 SERVER_URL 和 Channel。 |
||||
|
# ============================================================ |
||||
|
|
||||
|
# ---------- 后端地址(联调唯一必改项)---------- |
||||
|
# 客户端所有业务请求形如 POST {SERVER_URL}/api/home/<模块>_<方法>, |
||||
|
# 由 gateway 的 /api/:service/:method 路由(apps/services/modules/gateway/wservice_comp.go:66) |
||||
|
# 转发到对应服务的反射注册方法。 |
||||
|
# |
||||
|
# ym 测试环境:https://ym-dev.ymaikj.com (阿龙环境,当前默认) |
||||
|
# 本机联调: http://<你的局域网IP>:7100 (gateway HTTP 端口,见 deploy/app/confs/gateway.yaml.example) |
||||
|
# 真机调试别写 127.0.0.1/localhost —— 那是手机自己。 |
||||
|
SERVER_URL=https://ym-dev.ymaikj.com |
||||
|
|
||||
|
# 非中国大陆地区 / GOOGLE 渠道走这个地址(dio_manager.dart 的 baseUrl 逻辑)。 |
||||
|
# 留空会自动回退到 SERVER_URL,不会变成空 host。 |
||||
|
HW_SERVER_URL=https://ym-dev.ymaikj.com |
||||
|
|
||||
|
# ---------- 渠道 ---------- |
||||
|
# 取值见 lib/data/models/channel_app_model.dart 的 UserChannel(必须是那里 |
||||
|
# fromName 认识的名字,写 xiaomi 这种小写短名会被解析成 UNKNOWN)。 |
||||
|
# iOS 恒为 GLOBAL_APPLE,本项只对 Android 生效。 |
||||
|
# |
||||
|
# 国内 5 家商店(华为/荣耀/小米/OPPO/vivo)现在共用一个包:渠道在运行时按 |
||||
|
# 设备品牌推导(version_update_service.dart 的 getAndroidStoreInfoByBrand), |
||||
|
# 这里的值只是**兜底**——只作用于品牌认不出来的设备(三星、魅族等),决定它们 |
||||
|
# 归到哪一档渠道统计、拿哪一档的游客登录/跳过设备绑定开关。填任一国内渠道即可, |
||||
|
# 但后台 app_registry 里要给这一档配好。 |
||||
|
# |
||||
|
# 两个值不参与品牌推导,别拿来当国内包的兜底: |
||||
|
# GLOBAL_GOOGLE —— 海外包,装在华为手机上也必须继续连 HW_SERVER_URL; |
||||
|
# OFFICIAL —— 官方/内测包,agent 页面会多出"工具打断"调试菜单项。 |
||||
|
Channel=CHINA_XIAOMI |
||||
|
|
||||
|
# ---------- OpenAI 兼容网关(留空则由后端下发)---------- |
||||
|
OPENAI_BASE_URL= |
||||
|
OPENAI_API_KEY= |
||||
|
OPENAI_MODEL= |
||||
|
|
||||
|
# ---------- 火山引擎 语音(ASR / TTS,留空则由后端下发)---------- |
||||
|
VOLCANO_APP_KEY= |
||||
|
VOLCANO_SECRET_KEY= |
||||
|
VOLCANO_HOST_URL= |
||||
|
VOLCANO_SERVICE_ID= |
||||
|
VOLCANO_SPEECH_APP_ID= |
||||
|
VOLCANO_SPEECH_APP_TOKEN= |
||||
|
VOLCANO_ASR_RESOURCE_ID= |
||||
|
VOLCANO_TTS_APP_ID= |
||||
|
VOLCANO_TTS_APP_TOKEN= |
||||
|
VOLCANO_TTS_RESOURCE_ID= |
||||
|
# 只在 .env 里出现,Dart 代码未引用(疑似遗留) |
||||
|
VOLCANO_AI_API_KEY= |
||||
|
|
||||
|
# ---------- 火山引擎 翻译(留空则由后端下发)---------- |
||||
|
VOLCANO_TRANSLATION_ACCESS_KEY= |
||||
|
VOLCANO_TRANSLATION_SECRET_KEY= |
||||
|
VOLCANO_TRANSLATION_REGION= |
||||
|
|
||||
|
# ---------- iOS 内购商品 ID 拼装 ---------- |
||||
|
APP_IOS_IAP_PREFIX= |
||||
|
APP_IOS_IAP_SUFFIX= |
||||
|
APP_IOS_IAP_ID_MAP= |
||||
@ -0,0 +1,71 @@ |
|||||
|
# ============================================================ |
||||
|
# apps/client(Flutter,包名 eaimar) |
||||
|
# ⚠️ 首要目的:挡住 build/(5.2G)、ios/Pods(311M)、android/.gradle(120M) |
||||
|
# 这三坨构建产物——它们占了本目录 5.8G 中的 5.6G,误提交会直接毁掉仓库。 |
||||
|
# ============================================================ |
||||
|
|
||||
|
# ---- 杂项 ---- |
||||
|
.DS_Store |
||||
|
*.log |
||||
|
*.swp |
||||
|
*.pyc |
||||
|
__pycache__/ |
||||
|
.history |
||||
|
migrate_working_dir/ |
||||
|
|
||||
|
# ---- 编辑器 ---- |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
*.iws |
||||
|
.idea/ |
||||
|
.vscode/ |
||||
|
|
||||
|
# ---- Flutter / Dart / Pub ---- |
||||
|
# build/ 与 .dart_tool/ 不加前缀斜杠,才能同时覆盖 local_plugins/*/ 下的同名产物 |
||||
|
build/ |
||||
|
.dart_tool/ |
||||
|
.flutter-plugins |
||||
|
.flutter-plugins-dependencies |
||||
|
.packages |
||||
|
.pub/ |
||||
|
.pub-cache/ |
||||
|
pubspec_overrides.yaml |
||||
|
**/doc/api/ |
||||
|
**/ios/Flutter/.last_build_id |
||||
|
# 混淆符号表 |
||||
|
app.*.symbols |
||||
|
app.*.map.json |
||||
|
|
||||
|
# ---- Android ---- |
||||
|
.gradle/ |
||||
|
**/android/**/gradle-wrapper.jar |
||||
|
**/android/captures/ |
||||
|
**/android/gradlew |
||||
|
**/android/gradlew.bat |
||||
|
**/android/local.properties |
||||
|
**/android/**/GeneratedPluginRegistrant.java |
||||
|
**/android/key.properties |
||||
|
*.jks |
||||
|
|
||||
|
# ---- iOS ---- |
||||
|
**/ios/Pods/ |
||||
|
**/ios/.symlinks/ |
||||
|
**/ios/Flutter/ephemeral/ |
||||
|
**/ios/Flutter/App.framework |
||||
|
**/ios/Flutter/Flutter.framework |
||||
|
**/ios/Flutter/Flutter.podspec |
||||
|
**/ios/Flutter/Generated.xcconfig |
||||
|
**/ios/Flutter/flutter_export_environment.sh |
||||
|
**/ios/Flutter/flutter_assets/ |
||||
|
**/ios/ServiceDefinitions.json |
||||
|
**/ios/Runner/GeneratedPluginRegistrant.* |
||||
|
**/xcuserdata/ |
||||
|
*.mode1v3 |
||||
|
*.mode2v3 |
||||
|
*.moved-aside |
||||
|
*.pbxuser |
||||
|
*.perspectivev3 |
||||
|
|
||||
|
# ---- 本机私密配置 ---- |
||||
|
# .env 含火山/OpenAI/COS 等第三方密钥,只提交 .env.example |
||||
|
.env |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"mcpServers": { |
||||
|
"web-search": { |
||||
|
"url": "http://mcp.ymaikj.com:8000/sse" |
||||
|
}, |
||||
|
"mp3": { |
||||
|
"url": "https://api.ymaikj.com/sse" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
# This file tracks properties of this Flutter project. |
||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc. |
||||
|
# |
||||
|
# This file should be version controlled and should not be manually edited. |
||||
|
|
||||
|
version: |
||||
|
revision: "30ade5e937e6afbd2d2c38ec53175b89b556447e" |
||||
|
channel: "stable" |
||||
|
|
||||
|
project_type: app |
||||
|
|
||||
|
# Tracks metadata for the flutter migrate command |
||||
|
migration: |
||||
|
platforms: |
||||
|
- platform: root |
||||
|
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e |
||||
|
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e |
||||
|
- platform: ios |
||||
|
create_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e |
||||
|
base_revision: 30ade5e937e6afbd2d2c38ec53175b89b556447e |
||||
|
|
||||
|
# User provided section |
||||
|
|
||||
|
# List of Local paths (relative to this file) that should be |
||||
|
# ignored by the migrate tool. |
||||
|
# |
||||
|
# Files that are not part of the templates will be ignored by default. |
||||
|
unmanaged_files: |
||||
|
- 'lib/main.dart' |
||||
|
- 'ios/Runner.xcodeproj/project.pbxproj' |
||||
@ -0,0 +1,182 @@ |
|||||
|
# Google Play 上架待办清单 |
||||
|
|
||||
|
> 排查时间:2026-08-29。基线产物:`build/app/outputs/flutter-apk/app-release.apk`(1.0.1+4,Channel=CHINA_XIAOMI)。 |
||||
|
> 所有结论都是在**构建产物**上实测的(合并后的 AndroidManifest、APK 里的 ELF 段对齐),不是读源码猜的。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 0. 先决定:要不要申请延期 |
||||
|
|
||||
|
有两条硬期限是 **2026-08-31**,也就是排查次日。两条都可以在 Play Console 申请延期到 **2026-11-01**。 |
||||
|
|
||||
|
**建议先去 Play Console 把延期申请了**,再按下面的顺序改。否则 8/31 之后新包传不上去,改了也白改。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 1. 硬阻塞 |
||||
|
|
||||
|
### 1.1 targetSdk 35 → 36(期限 2026-08-31) |
||||
|
|
||||
|
- 位置:[android/app/build.gradle.kts](android/app/build.gradle.kts) `defaultConfig.targetSdk = 35` |
||||
|
- `compileSdk` 已经是 36,改一个数字即可 |
||||
|
- ⚠️ 但 Android 16 有行为变更,改完**必须真机回归**: |
||||
|
- 强制 edge-to-edge(状态栏/导航栏不再自动留白,页面可能被遮) |
||||
|
- 方向与尺寸可调整性 API 变更(大屏/折叠屏) |
||||
|
- 前台服务类型校验更严 |
||||
|
|
||||
|
### 1.2 Play Billing 7.1.1 → 8.0+(期限 2026-08-31) |
||||
|
|
||||
|
- 当前:`in_app_purchase_android` 依赖 `com.android.billingclient:billing:7.1.1` |
||||
|
- 要升到带 Billing Library 8 的 `in_app_purchase_android` 版本 |
||||
|
- ⚠️ BL8 有破坏性 API 变更,是这份清单里**改动量最大**的一项。来不及就务必申请延期。 |
||||
|
- 迁移指南:https://developer.android.com/google/play/billing/migrate-gpblv8 |
||||
|
|
||||
|
### 1.3 16KB 页对齐(已于 2025-11-01 生效,无延期) |
||||
|
|
||||
|
实测 `arm64-v8a` 共 26 个 `.so`,**3 个不合规**(LOAD 段 p_align = 0x1000,需 ≥ 0x4000): |
||||
|
|
||||
|
| 库 | 来源 | 处理 | |
||||
|
|---|---|---| |
||||
|
| `libAMapSDK_NAVI_v10_0_800.so` | 高德导航(预编译) | ✅ 已处理——[build.gradle.kts](android/app/build.gradle.kts) 的 `jniLibs.excludes` 在 `Channel=GLOBAL_GOOGLE` 时排除 `**/libAMapSDK*.so` | |
||||
|
| `libg722_jni.so` | **自己的源码** | ❌ 待修 | |
||||
|
| `libmsbc_jni.so` | **自己的源码** | ❌ 待修 | |
||||
|
|
||||
|
后两个来自 [local_plugins/bluetooth_manager/android/src/main/cpp](local_plugins/bluetooth_manager/android/src/main/cpp)(`g722_jni.cpp` / `msbc_jni.c`,CMake 工程),**源码在仓库里,好修**。 |
||||
|
|
||||
|
根因:该插件的 `android/build.gradle` **没有声明 `ndkVersion`**,所以没继承主工程的 NDK `28.2.13676358`。NDK r28 起默认就是 16KB 对齐,用了老 NDK 才退回 4KB。 |
||||
|
|
||||
|
两种改法(建议都加,双保险): |
||||
|
|
||||
|
```gradle |
||||
|
// local_plugins/bluetooth_manager/android/build.gradle |
||||
|
android { |
||||
|
ndkVersion = "28.2.13676358" // 与主工程一致,r28 默认 16KB 对齐 |
||||
|
|
||||
|
defaultConfig { |
||||
|
externalNativeBuild { |
||||
|
cmake { |
||||
|
cppFlags "" |
||||
|
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // NDK r27+ 支持 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
兜底(任何 NDK 版本都生效),在 `src/main/cpp/CMakeLists.txt` 里给两个 target 加: |
||||
|
|
||||
|
```cmake |
||||
|
target_link_options(g722_jni PRIVATE "-Wl,-z,max-page-size=16384") |
||||
|
target_link_options(msbc_jni PRIVATE "-Wl,-z,max-page-size=16384") |
||||
|
``` |
||||
|
|
||||
|
⚠️ 改完要清 `local_plugins/bluetooth_manager/android/.cxx/`(里面有旧的构建缓存),否则可能直接复用旧产物。 |
||||
|
|
||||
|
**armeabi-v7a 的不合规项不用管**:16KB 只约束 64 位(arm64-v8a / x86_64)。v7a 里的 `libneonui_shared.so`、`libneonuijni_public.so` 是 32 位专有,不影响上架。 |
||||
|
|
||||
|
**验证方式**(仓库里已有现成脚本): |
||||
|
|
||||
|
```bash |
||||
|
python3 check_16kb.py build/app/outputs/bundle/release/app-release.aab |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 2. 政策风险:支付回退逻辑(优先级仅次于硬阻塞) |
||||
|
|
||||
|
**这是最容易被下架的一条。** |
||||
|
|
||||
|
[lib/modules/goods/controllers/goods_base_controller.dart](lib/modules/goods/controllers/goods_base_controller.dart) 的 `_getEnabledPayMethods()`:当后端没下发 `paychannels` 时,回退成展示 **微信 + 支付宝 + PayPal + Google**。 |
||||
|
|
||||
|
```dart |
||||
|
// 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式 |
||||
|
if (methods.isEmpty) { |
||||
|
final fallback = <_PayMethod>[ |
||||
|
_PayMethod.wechat, |
||||
|
_PayMethod.alipay, |
||||
|
_PayMethod.paypal |
||||
|
]; |
||||
|
... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
数字商品在 Google Play 必须走 Play 结算,摆出微信/支付宝是最典型的下架理由。而且这个回退**恰好在配置缺失时触发**——排查当天测试环境就正处于这个状态(渠道配置没建,`paychannels` 是空的)。 |
||||
|
|
||||
|
两件事要一起做: |
||||
|
|
||||
|
1. **改回退逻辑**:安卓端兜底只留 `google`(iOS 只留 `apple`),不要「拿不到配置就全展示」 |
||||
|
2. **后台配置**:console 后台给 `GLOBAL_GOOGLE` 渠道的 `paychannels` 只填 `google` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 3. 权限瘦身 |
||||
|
|
||||
|
合并后的 AndroidManifest 共 44 条权限,下面这些会被 Play 拦或需要额外声明: |
||||
|
|
||||
|
| 权限 | 处理建议 | |
||||
|
|---|---| |
||||
|
| `QUERY_ALL_PACKAGES` | 需提交声明表;通常要求改成 `<queries>` 精确列出微信/支付宝包名(来源应是 fluwx / tobias) | |
||||
|
| `ACCESS_BACKGROUND_LOCATION` | 需声明表 + 演示视频,审核最严。**先确认是否真需要后台定位**,不需要就删 | |
||||
|
| `SYSTEM_ALERT_WINDOW` | 悬浮窗(`floating_ui_plugin`),需说明用途 | |
||||
|
| `WRITE_SETTINGS` | 很少被批准,确认是否真需要 | |
||||
|
| `SCHEDULE_EXACT_ALARM` | 有闹钟功能可解释,准备好说明 | |
||||
|
| `MEDIA_CONTENT_CONTROL` | **建议直接删**——这是系统签名权限,普通应用根本拿不到,声明了也无效,反而显得可疑 | |
||||
|
| `GET_TASKS` | **建议直接删**——API 21 起已废弃并被系统忽略 | |
||||
|
|
||||
|
源清单里的 `SEND_SMS`、`MANAGE_EXTERNAL_STORAGE` 已经是注释状态,**合并产物里没有**,不用担心。 |
||||
|
|
||||
|
查合并后清单的命令: |
||||
|
|
||||
|
```bash |
||||
|
AAPT=$(ls ~/Library/Android/sdk/build-tools/*/aapt2 | tail -1) |
||||
|
$AAPT dump permissions build/app/outputs/flutter-apk/app-release.apk | grep '^uses-permission' |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 4. 构建流程的坑 |
||||
|
|
||||
|
- **`.env` 的 `Channel` 必须是 `GLOBAL_GOOGLE`**(当前是 `CHINA_XIAOMI`)。 |
||||
|
`./build_channels.sh google` 会自动改;手工跑 `flutter build appbundle` **不会**,那样 AMap 的 `.so` 也不会被排除 → 16KB 检查直接挂。 |
||||
|
- **[build_channels.sh](build_channels.sh) 的 `OUTPUT_DIR` 写死成 `/Users/liuxiamin/work/build_outputs`**,本机不存在,脚本会失败。改成本机路径或改成相对路径。 |
||||
|
- Google 渠道排除了 AMap `.so`,但**调用代码还在**。要验证海外包点到地图/导航相关功能不会崩。 |
||||
|
- 产物是 **AAB 不是 APK**(`build_channels.sh` 的 google 分支已经处理成 `aab`)。 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 5. Play Console 首次上架事项 |
||||
|
|
||||
|
- Play App Signing(上传密钥用 `android/app/sign/eaimar.keystore`) |
||||
|
- 数据安全表单(Data safety) |
||||
|
- 内容分级问卷 |
||||
|
- 目标受众与内容声明 |
||||
|
- 隐私政策 URL —— iOS 那边已在用 `https://yimai.games/privacy.html`,可复用 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 建议的动手顺序 |
||||
|
|
||||
|
1. **去 Play Console 申请 targetSdk 与 Billing 的延期**(两条都是 8/31 到期) |
||||
|
2. 16KB 对齐(改 1 个 gradle + 1 个 CMakeLists,最确定,改完用 `check_16kb.py` 验) |
||||
|
3. 支付回退逻辑 + 后台 `paychannels` 配置 |
||||
|
4. 权限瘦身(先删 `MEDIA_CONTENT_CONTROL` / `GET_TASKS`,再处理 `QUERY_ALL_PACKAGES`) |
||||
|
5. `build_channels.sh` 的 `OUTPUT_DIR` 改本机路径 |
||||
|
6. targetSdk 36 + 真机回归 |
||||
|
7. Billing 8 迁移(改动最大,放最后) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 附:本次会话的其他遗留(与 Google Play 无关) |
||||
|
|
||||
|
- **iOS 包 `1.0.1 (5)` 已打好待上传**:`build/ios/ipa/EAIMAR.ipa`。已修掉通讯录缺用途说明(ITMS-90683),并移除了 ATT/IDFA(`NSUserTrackingUsageDescription` 等三个 key、`app_tracking_transparency` 依赖、splash 里的授权请求)。 |
||||
|
⚠️ 打 iOS 包要带上自备的 ExportOptions(`manageAppVersionAndBuildNumber=false`),否则构建号会被 Xcode 自动改掉: |
||||
|
```bash |
||||
|
flutter build ipa --release --export-options-plist=<path>/ExportOptions.plist |
||||
|
``` |
||||
|
⚠️ 只要动了 `ios/Runner/Info.plist` 里的 `*UsageDescription`,就必须清一次 DerivedData 与 SwiftPM 清单缓存,否则 `permission_handler` 会按缓存里的旧宏编译,留下无用途说明的系统 API 引用 → 又一次 ITMS-90683: |
||||
|
```bash |
||||
|
rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* |
||||
|
rm -rf ~/Library/Caches/org.swift.swiftpm/manifests ~/Library/org.swift.swiftpm/cache |
||||
|
``` |
||||
|
- **游客登录版本开关已上线到阿龙测试环境**(app/console/admin 三个服务),但**后台渠道配置还没建**,所以入口目前一定是隐藏的。要验证需在 console 后台「全局默认」作用域下配 App Store 与 荣耀 两条,生效版本填 `1.0.1`。详见 [CLAUDE.md](../../CLAUDE.md) 的「游客登录入口的显隐」一节。 |
||||
|
- **两台测试机装的是 `1.0.1 (4)`**(含 ATT 的旧版),需要的话要重装 `(5)`。 |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>API_KEY</key> |
||||
|
<string>AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE</string> |
||||
|
<key>GCM_SENDER_ID</key> |
||||
|
<string>733281995296</string> |
||||
|
<key>PLIST_VERSION</key> |
||||
|
<string>1</string> |
||||
|
<key>BUNDLE_ID</key> |
||||
|
<string>com.saitong.voitrans</string> |
||||
|
<key>PROJECT_ID</key> |
||||
|
<string>voitrans</string> |
||||
|
<key>STORAGE_BUCKET</key> |
||||
|
<string>voitrans.firebasestorage.app</string> |
||||
|
<key>IS_ADS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_ANALYTICS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_APPINVITE_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_GCM_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_SIGNIN_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>GOOGLE_APP_ID</key> |
||||
|
<string>1:733281995296:ios:6a8351691312100bd70a45</string> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,51 @@ |
|||||
|
# I18N Placeholder Remediation |
||||
|
|
||||
|
This document records the repeatable steps used to detect and fix leftover English placeholder text and invalid-quote issues in translation map files under `lib/core/translations/language/`. |
||||
|
|
||||
|
## Problem Patterns |
||||
|
|
||||
|
### 1) English placeholders inside non-English locales |
||||
|
|
||||
|
Symptoms: |
||||
|
- UI shows English strings in a non-English locale. |
||||
|
- `tools/i18n_audit.py --mode englishy` reports items for a locale file. |
||||
|
|
||||
|
Fix: |
||||
|
- Replace English values with accurate localized text. |
||||
|
- Keep placeholder syntax intact (examples: `@minutes`, `{count}`, `{filePath}`, `{error}`). |
||||
|
|
||||
|
### 2) Invalid “smart quotes” used as Dart string delimiters |
||||
|
|
||||
|
Symptoms: |
||||
|
- `flutter analyze` reports `illegal_character`, `expected_token`, `expression_in_map`, `undefined_identifier` in a language file. |
||||
|
|
||||
|
Root cause: |
||||
|
- Curly quotes like `‘ ’` are not valid Dart string delimiters, so map entries stop being parsed as strings. |
||||
|
|
||||
|
Fix: |
||||
|
- Replace `‘` and `’` with ASCII `'` and ensure every entry is in the form `'key': 'value',`. |
||||
|
|
||||
|
## Verification Checklist |
||||
|
|
||||
|
Run these commands from the repo root: |
||||
|
|
||||
|
```bash |
||||
|
python3 tools/i18n_audit.py --mode englishy |
||||
|
flutter analyze lib/core/translations/language/<locale>.dart |
||||
|
dart format lib/core/translations/language/<locale>.dart |
||||
|
flutter test test/<locale>_translations_test.dart |
||||
|
``` |
||||
|
|
||||
|
Recommended additional checks: |
||||
|
|
||||
|
```bash |
||||
|
rg "[‘’]" lib/core/translations/language/<locale>.dart |
||||
|
``` |
||||
|
|
||||
|
## Test Convention |
||||
|
|
||||
|
For each locale file, add a `test/<locale>_translations_test.dart` test that asserts: |
||||
|
- The map can be imported and accessed. |
||||
|
- Its key set matches `enUS`. |
||||
|
- It contains no empty string values. |
||||
|
|
||||
@ -0,0 +1,60 @@ |
|||||
|
# Deep Voice |
||||
|
|
||||
|
Flutter 应用程序,使用 GetX 状态管理和依赖注入。 |
||||
|
|
||||
|
## 项目目录结构 |
||||
|
|
||||
|
``` |
||||
|
lib/ |
||||
|
├─ core/ |
||||
|
│ ├─ bindings/ # 全局或通用的 Bindings |
||||
|
│ ├─ constants/ # 常量、枚举、Key、全局配置等 |
||||
|
│ ├─ errors/ # 异常/错误处理相关 |
||||
|
│ ├─ themes/ # 主题(Color, TextStyle 等) |
||||
|
│ ├─ translations/ # 国际化、语言相关 |
||||
|
│ ├─ utils/ # 通用工具类、Helper、扩展函数等 |
||||
|
│ └─ widgets/ # 通用的无业务逻辑的Widgets |
||||
|
│ |
||||
|
├─ data/ |
||||
|
│ ├─ models/ # 数据模型(可与后端DTO对应) |
||||
|
│ ├─ repositories/ # 数据仓库(实际操作数据 CRUD, 远程或本地) |
||||
|
│ ├─ services/ # 第三方或原生平台封装,如网络请求、缓存、数据库等 |
||||
|
│ └─ providers/ # 适配器层,比如把网络、数据库等转换为 Repository 可调用的接口 |
||||
|
│ |
||||
|
├─ modules/ |
||||
|
│ ├─ login/ |
||||
|
│ │ ├─ controllers/ # GetX Controller |
||||
|
│ │ ├─ views/ # 页面、Widget |
||||
|
│ │ ├─ bindings/ # 该模块的 Bindings |
||||
|
│ │ └─ ... # 其他文件 |
||||
|
│ ├─ home/ |
||||
|
│ │ ├─ controllers/ |
||||
|
│ │ ├─ views/ |
||||
|
│ │ ├─ bindings/ |
||||
|
│ │ └─ ... |
||||
|
│ └─ ... |
||||
|
│ |
||||
|
├─ routes/ |
||||
|
│ ├─ app_pages.dart # 定义所有页面的 Route 列表 |
||||
|
│ └─ app_routes.dart # 定义路由常量或路由字符串 |
||||
|
│ |
||||
|
└─ main.dart # 程序入口 |
||||
|
``` |
||||
|
### 本地调试内购(IAP)流程 |
||||
|
|
||||
|
不需要真实购买即可在本地跑通前端内购流程: |
||||
|
|
||||
|
- 在项目根目录的环境变量文件(如 `.env`)里添加: |
||||
|
|
||||
|
``` |
||||
|
APP_IAP_MOCK=1 |
||||
|
``` |
||||
|
|
||||
|
- 作用:在 `PayService` 中启用“本地内购模拟”,调用 `processGooglePayment` 或 `processApplePayment` 会: |
||||
|
- 立即触发“已发起内购(本地模拟)”; |
||||
|
- 短暂延时后触发“支付处理中…(本地模拟)”; |
||||
|
- 再触发“支付成功(本地模拟)”。 |
||||
|
|
||||
|
- 注意:该模式不依赖 Google Play / App Store 安装来源,也不会进行服务器收据校验,仅用于前端 UI 与状态机验证。 |
||||
|
|
||||
|
- 关闭模拟:将 `APP_IAP_MOCK` 移除或设为 `0/false`,恢复真实内购流程。 |
||||
@ -0,0 +1,27 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>AvailableLibraries</key> |
||||
|
<array> |
||||
|
<dict> |
||||
|
<key>BinaryPath</key> |
||||
|
<string>SpeexKit.framework/SpeexKit</string> |
||||
|
<key>LibraryIdentifier</key> |
||||
|
<string>ios-arm64</string> |
||||
|
<key>LibraryPath</key> |
||||
|
<string>SpeexKit.framework</string> |
||||
|
<key>SupportedArchitectures</key> |
||||
|
<array> |
||||
|
<string>arm64</string> |
||||
|
</array> |
||||
|
<key>SupportedPlatform</key> |
||||
|
<string>ios</string> |
||||
|
</dict> |
||||
|
</array> |
||||
|
<key>CFBundlePackageType</key> |
||||
|
<string>XFWK</string> |
||||
|
<key>XCFrameworkFormatVersion</key> |
||||
|
<string>1.0</string> |
||||
|
</dict> |
||||
|
</plist> |
||||
@ -0,0 +1,44 @@ |
|||||
|
//
|
||||
|
// OpusTools.h
|
||||
|
// SpeexKit
|
||||
|
//
|
||||
|
// Created by EzioChan on 2024/1/17.
|
||||
|
// Copyright © 2024 www.zh-jieli.com. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
|
||||
|
NS_ASSUME_NONNULL_BEGIN |
||||
|
|
||||
|
@protocol OpusToolsDelegate <NSObject> |
||||
|
|
||||
|
-(void)opusToolsPcmData:(NSData*)data; |
||||
|
|
||||
|
-(void)opusToolsPcmFile:(NSData *)data; |
||||
|
|
||||
|
-(void)opusToolsOpusData:(NSData *)data; |
||||
|
|
||||
|
-(void)opusToolsOpusFile:(NSData *)data; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface OpusTools : NSObject |
||||
|
|
||||
|
@property(nonatomic,weak)id<OpusToolsDelegate> delegate; |
||||
|
|
||||
|
-(void)opusDecodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int) fs isHasHead:(BOOL)head; |
||||
|
|
||||
|
-(void)opusEncodeInfo:(int)sampleRate Channel:(int)channel frameSize:(int)fs isHasHead:(BOOL)head; |
||||
|
|
||||
|
-(void)decodeData:(NSData*)data; |
||||
|
|
||||
|
-(void)decodeFile:(NSString*)path; |
||||
|
|
||||
|
-(void)encodeData:(NSData*)data; |
||||
|
|
||||
|
-(void)encodeFile:(NSString*)path; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
NS_ASSUME_NONNULL_END |
||||
@ -0,0 +1,100 @@ |
|||||
|
//
|
||||
|
// OpusUnit.h
|
||||
|
// QCY_Demo
|
||||
|
//
|
||||
|
// Created by 杰理科技 on 2021/7/5.
|
||||
|
// Copyright © 2021 杰理科技. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
NS_ASSUME_NONNULL_BEGIN |
||||
|
|
||||
|
/**
|
||||
|
* 通知:流式编码回调Opus数据 |
||||
|
* 通知数据类型:NSData(Opus) |
||||
|
*/ |
||||
|
extern NSString *kOPUS_ENCODE_DATA; |
||||
|
/**
|
||||
|
* 通知:流式解码回调PCM数据 |
||||
|
* 通知数据类型:NSData(PCM) |
||||
|
*/ |
||||
|
extern NSString *kOPUS_DECODE_DATA; |
||||
|
|
||||
|
|
||||
|
@interface OpusUnit : NSObject |
||||
|
|
||||
|
+ (void)opusIsLog:(BOOL)log; |
||||
|
|
||||
|
#pragma mark - Opus参数 |
||||
|
/**
|
||||
|
* 设置参数 |
||||
|
* @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000) |
||||
|
* @param frameRate 帧长度 |
||||
|
* 16k :40 |
||||
|
* 32K :80 |
||||
|
* 64k :160 |
||||
|
* 96k :320 |
||||
|
* 128k:640 |
||||
|
* @param channel 声道 例如:1、2 (默认 1) |
||||
|
* @param isHeader 是否带头 |
||||
|
*/ |
||||
|
+(void)opusSetSampleRate:(int)rate frameRate:(int)frameRate Channels:(int)channel isHeader:(BOOL)isHeader; |
||||
|
|
||||
|
#pragma mark - Opus解码 |
||||
|
|
||||
|
/**
|
||||
|
* 直接【opus文件】转换成【pcm文件】 |
||||
|
* @param path_opus opus文件路径 |
||||
|
* @param path_pcm pcm文件路径 |
||||
|
*/ |
||||
|
+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【开启】 |
||||
|
* 注意:需要异步调用,放在工程单利类中,切勿频繁开启。 |
||||
|
*/ |
||||
|
+ (int)opusDecoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入Opus的数据 |
||||
|
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
||||
|
* 通知数据类型:NSNotification.object => NSData(PCM格式) |
||||
|
* @param data opus数据流(长度:1024) |
||||
|
*/ |
||||
|
+ (void)opusWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【关闭】 |
||||
|
*/ |
||||
|
+ (int)opusDecoderStop; |
||||
|
|
||||
|
#pragma mark - Opus编码 |
||||
|
|
||||
|
/**
|
||||
|
* 【pcm文件】转换成【opus文件】 |
||||
|
*/ |
||||
|
+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus; |
||||
|
|
||||
|
/**
|
||||
|
* 流式编码【开启】 |
||||
|
* 注意:需要异步调用,放在工程单利类中,切勿频繁开启。 |
||||
|
*/ |
||||
|
+ (int)opusEncoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入pcm的数据 |
||||
|
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
||||
|
* 通知数据类型:NSNotification.object => NSData(opus格式) |
||||
|
* @param data opus数据流(长度:1024) |
||||
|
*/ |
||||
|
+ (void)pcmWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式编码【关闭】 |
||||
|
*/ |
||||
|
+ (int)opusEncoderStop; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
NS_ASSUME_NONNULL_END |
||||
@ -0,0 +1,21 @@ |
|||||
|
//
|
||||
|
// SpeexKit.h
|
||||
|
// SpeexKit
|
||||
|
//
|
||||
|
// Created by zhihui liang on 2018/8/20.
|
||||
|
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
//! Project version number for SpeexKit.
|
||||
|
FOUNDATION_EXPORT double SpeexKitVersionNumber; |
||||
|
|
||||
|
//! Project version string for SpeexKit.
|
||||
|
FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[]; |
||||
|
|
||||
|
// In this header, you should import all the public headers of your framework using statements like #import <SpeexKit/PublicHeader.h>
|
||||
|
|
||||
|
#import <SpeexKit/SpeexUnit.h> |
||||
|
#import <SpeexKit/OpusUnit.h> |
||||
|
#import <SpeexKit/OpusTools.h> |
||||
@ -0,0 +1,40 @@ |
|||||
|
//
|
||||
|
// SpeexUnit.h
|
||||
|
// SpeexKit
|
||||
|
//
|
||||
|
// Created by zhihui liang on 2018/8/20.
|
||||
|
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码回调PCM数据 |
||||
|
*/ |
||||
|
extern NSString *kSPEEX_DECODE_DATA; //speex decode
|
||||
|
|
||||
|
@interface SpeexUnit : NSObject |
||||
|
|
||||
|
+(void)speexIsLog:(BOOL)log; |
||||
|
|
||||
|
/**
|
||||
|
* 直接【speex文件】转换成【pcm文件】 |
||||
|
*/ |
||||
|
+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【开启】 |
||||
|
*/ |
||||
|
+(int)speexDecoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入Speex的数据 |
||||
|
*/ |
||||
|
+(void)speexWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【关闭】 |
||||
|
*/ |
||||
|
+(int)speexDecoderStop; |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,6 @@ |
|||||
|
framework module SpeexKit { |
||||
|
umbrella header "SpeexKit.h" |
||||
|
export * |
||||
|
|
||||
|
module * { export * } |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
include: package:flutter_lints/flutter.yaml |
||||
@ -0,0 +1,16 @@ |
|||||
|
gradle-wrapper.jar |
||||
|
/.gradle |
||||
|
/captures/ |
||||
|
/gradlew |
||||
|
/gradlew.bat |
||||
|
/local.properties |
||||
|
GeneratedPluginRegistrant.java |
||||
|
.cxx/ |
||||
|
|
||||
|
# Remember to never publicly share your keystore. |
||||
|
# See https://flutter.dev/to/reference-keystore |
||||
|
key.properties |
||||
|
**/*.keystore |
||||
|
**/*.jks |
||||
|
# Exception: keep the Eaimar release keystore tracked in git (intentional) |
||||
|
!app/sign/eaimar.keystore |
||||
@ -0,0 +1,164 @@ |
|||||
|
import java.util.Properties |
||||
|
import java.io.FileInputStream |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
flatDir { |
||||
|
dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行 |
||||
|
dirs(project(":device_jieli").projectDir.resolve("libs")) // 杰理 SDK 运行时 .aar |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
plugins { |
||||
|
id("com.android.application") |
||||
|
id("kotlin-android") |
||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. |
||||
|
id("dev.flutter.flutter-gradle-plugin") |
||||
|
id("com.google.gms.google-services")version "4.3.13" |
||||
|
} |
||||
|
|
||||
|
val keystorePropertiesFile = rootProject.file("app/sign/Eaimar.properties") |
||||
|
val keystoreProperties =Properties().apply { |
||||
|
load(FileInputStream(keystorePropertiesFile)) // 使用 Kotlin 扩展方法 |
||||
|
} |
||||
|
|
||||
|
android { |
||||
|
namespace = "com.yimai.eaimar" |
||||
|
compileSdk = 36 |
||||
|
ndkVersion = "28.2.13676358" |
||||
|
|
||||
|
compileOptions { |
||||
|
sourceCompatibility = JavaVersion.VERSION_11 |
||||
|
targetCompatibility = JavaVersion.VERSION_11 |
||||
|
// Enable core library desugaring |
||||
|
isCoreLibraryDesugaringEnabled = true |
||||
|
} |
||||
|
|
||||
|
kotlinOptions { |
||||
|
jvmTarget = JavaVersion.VERSION_11.toString() |
||||
|
} |
||||
|
|
||||
|
// 添加lint选项 |
||||
|
lintOptions { |
||||
|
isCheckReleaseBuilds = false |
||||
|
} |
||||
|
|
||||
|
// 添加packaging配置,排除冲突文件 |
||||
|
packaging { |
||||
|
resources { |
||||
|
excludes.add("META-INF/DEPENDENCIES") |
||||
|
excludes.add("META-INF/LICENSE") |
||||
|
excludes.add("META-INF/LICENSE.txt") |
||||
|
excludes.add("META-INF/license.txt") |
||||
|
excludes.add("META-INF/NOTICE") |
||||
|
excludes.add("META-INF/NOTICE.txt") |
||||
|
excludes.add("META-INF/notice.txt") |
||||
|
excludes.add("META-INF/*.kotlin_module") |
||||
|
} |
||||
|
jniLibs { |
||||
|
useLegacyPackaging = true |
||||
|
// Google Play 渠道排除高德地图 .so(尚未支持 16KB 页面对齐) |
||||
|
val envFile = rootProject.file("../.env") |
||||
|
if (envFile.exists()) { |
||||
|
val channel = envFile.readLines() |
||||
|
.firstOrNull { it.trim().startsWith("Channel=") } |
||||
|
?.substringAfter("=")?.trim() |
||||
|
if (channel == "GLOBAL_GOOGLE") { |
||||
|
excludes += setOf("**/libAMapSDK*.so") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
defaultConfig { |
||||
|
manifestPlaceholders.putAll( |
||||
|
mapOf( |
||||
|
"applicationName" to "com.yimai.eaimar.MyApp" // 新增:声明自定义 Application |
||||
|
) |
||||
|
) |
||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
||||
|
applicationId = "com.yimai.eaimar" |
||||
|
// You can update the following values to match your application needs. |
||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config. |
||||
|
minSdk = 26 // 更新为26以满足ble_service库的要求 |
||||
|
targetSdk = 35 // 设置为 Android 15 (API 35),以满足应用市场要求 |
||||
|
versionCode = flutter.versionCode // 对应构建号,自动同步flutter构建号 |
||||
|
versionName = flutter.versionName // 对应版本号,自动同步flutter版本号 |
||||
|
} |
||||
|
|
||||
|
signingConfigs { |
||||
|
create("release") { |
||||
|
keyAlias = keystoreProperties.getProperty("keyAlias") |
||||
|
keyPassword = keystoreProperties.getProperty("keyPassword") |
||||
|
storeFile = file(keystoreProperties.getProperty("storeFile")) |
||||
|
storePassword = keystoreProperties.getProperty("storePassword") |
||||
|
enableV1Signing = true |
||||
|
enableV2Signing = true |
||||
|
enableV3Signing = false |
||||
|
enableV4Signing = false |
||||
|
} |
||||
|
getByName("debug") { |
||||
|
// debug 使用与 release 相同的签名配置 |
||||
|
keyAlias = keystoreProperties.getProperty("keyAlias") |
||||
|
keyPassword = keystoreProperties.getProperty("keyPassword") |
||||
|
storeFile = file(keystoreProperties.getProperty("storeFile")) |
||||
|
storePassword = keystoreProperties.getProperty("storePassword") |
||||
|
enableV1Signing = true |
||||
|
enableV2Signing = true |
||||
|
enableV3Signing = false |
||||
|
enableV4Signing = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buildTypes { |
||||
|
release { |
||||
|
// TODO: Add your own signing config for the release build. |
||||
|
// Signing with the debug keys for now, so `flutter run --release` works. |
||||
|
isMinifyEnabled = false |
||||
|
isShrinkResources = false |
||||
|
proguardFiles( |
||||
|
getDefaultProguardFile("proguard-android-optimize.txt"), |
||||
|
"proguard-rules.pro" |
||||
|
) |
||||
|
signingConfig = signingConfigs.getByName("release") |
||||
|
|
||||
|
// Release 版本只保留 arm64-v8a 和 armeabi-v7a(移除 x86/x86_64) |
||||
|
ndk { |
||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a") |
||||
|
} |
||||
|
} |
||||
|
debug { |
||||
|
isMinifyEnabled = false |
||||
|
isShrinkResources = false |
||||
|
proguardFiles( |
||||
|
getDefaultProguardFile("proguard-android-optimize.txt"), |
||||
|
"proguard-rules.pro" |
||||
|
) |
||||
|
signingConfig = signingConfigs.getByName("debug") |
||||
|
|
||||
|
// Debug 版本保留所有 ABI(包括 x86/x86_64,用于模拟器) |
||||
|
ndk { |
||||
|
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
// 添加核心库反糖化 |
||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") |
||||
|
|
||||
|
// 添加 CardView 依赖 |
||||
|
implementation("androidx.cardview:cardview:1.0.0") |
||||
|
|
||||
|
// device_jieli 杰理 SDK 运行时依赖(.aar 在 device_jieli 模块只能 compileOnly, |
||||
|
// host app 必须自己引入,否则 RCSPController 等类在运行时找不到 → scanFeature 不会被初始化) |
||||
|
implementation(":jl_bluetooth_rcsp_V4.2.0_beta1_40255_20260324@aar") |
||||
|
implementation(":jl_audio_v2_V1.0.0_9-release@aar") |
||||
|
implementation(":jldecryption_v0.4-release@aar") |
||||
|
// jl_audio_decode 已经被 ble_service 通过 com.jieli.audio:jl_audio_decode:2.1.0 引入,无需重复 |
||||
|
} |
||||
|
|
||||
|
flutter { |
||||
|
source = "../.." |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
{ |
||||
|
"project_info": { |
||||
|
"project_number": "733281995296", |
||||
|
"project_id": "voitrans", |
||||
|
"storage_bucket": "voitrans.firebasestorage.app" |
||||
|
}, |
||||
|
"client": [ |
||||
|
{ |
||||
|
"client_info": { |
||||
|
"mobilesdk_app_id": "1:733281995296:android:fd51ec5604787ab7d70a45", |
||||
|
"android_client_info": { |
||||
|
"package_name": "com.yimai.eaimar" |
||||
|
} |
||||
|
}, |
||||
|
"oauth_client": [], |
||||
|
"api_key": [ |
||||
|
{ |
||||
|
"current_key": "AIzaSyDPvThoWXxZ-LhE0gS9KH51jRE3f8YTmOE" |
||||
|
} |
||||
|
], |
||||
|
"services": { |
||||
|
"appinvite_service": { |
||||
|
"other_platform_oauth_client": [] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"configuration_version": "1" |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
# 移除系统组件的日志调用 |
||||
|
-assumenosideeffects class android.util.Log { |
||||
|
public static int v(...); |
||||
|
public static int d(...); |
||||
|
public static int i(...); |
||||
|
} |
||||
|
|
||||
|
# 针对特定类的日志移除 |
||||
|
-assumenosideeffects class android.media.AudioTrack { |
||||
|
private static void log*(...); |
||||
|
} |
||||
|
|
||||
|
-assumenosideeffects class android.app.AppOpsManager { |
||||
|
private static void log*(...); |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
# Flutter Framework |
||||
|
# 这些规则保留了 Flutter 引擎和插件系统正常运行所需的类。 |
||||
|
-ignorewarnings |
||||
|
-keep class io.flutter.app.** { *; } |
||||
|
-keep class io.flutter.plugin.** { *; } |
||||
|
-keep class io.flutter.util.** { *; } |
||||
|
-keep class io.flutter.view.** { *; } |
||||
|
-keep class io.flutter.embedding.android.** { *; } |
||||
|
-keep class io.flutter.embedding.engine.** { *; } |
||||
|
-keep class io.flutter.embedding.engine.plugins.** { *; } |
||||
|
-keep class io.flutter.plugin.common.** { *; } |
||||
|
-keep class io.flutter.plugins.** { *; } |
||||
|
-keep class io.flutter.plugin.platform.** { *; } |
||||
|
|
||||
|
# 保留你的应用程序的主 Activity。 |
||||
|
# !!! 请将 com.example.deep_voice.MainActivity 替换为你的实际包名和 Activity 类名 !!! |
||||
|
-keep class com.yimai.eaimar.MainActivity { *; } |
||||
|
|
||||
|
# 保留 open_ai_service 包下的所有类 |
||||
|
-keep class com.yunqiinnovation.open_ai_service.** { *; } |
||||
|
|
||||
|
# 保留 MCPClient 相关类 |
||||
|
-keep class com.yunqiinnovation.open_ai_service.MCPClient { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.MCPSubClient { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.FunctionHandler { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.ToolType { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.mcp.** { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.SystemFunctionHandler { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.OpenAIService { *; } |
||||
|
-keep class com.yunqiinnovation.open_ai_service.OpenAIService$* { *; } |
||||
|
|
||||
|
# 保留 OpenAIServicePlugin 类 |
||||
|
-keep class com.yunqiinnovation.open_ai_service.OpenAIServicePlugin { *; } |
||||
|
|
||||
|
# 保留 Jackson 相关类 |
||||
|
-keep class com.fasterxml.jackson.** { *; } |
||||
|
-keep class com.fasterxml.jackson.databind.** { *; } |
||||
|
-keep class com.fasterxml.jackson.core.** { *; } |
||||
|
|
||||
|
# 保留 Ktor 相关类 |
||||
|
-keep class io.ktor.** { *; } |
||||
|
-keep class kotlinx.coroutines.** { *; } |
||||
|
-keep class kotlinx.serialization.** { *; } |
||||
|
|
||||
|
# 如果你的应用直接使用了 AndroidX 的某些特定类(例如,通过原生代码或插件),可能需要保留它们。 |
||||
|
# 例如,保留所有 Parcelable 实现类,如果它们被跨进程使用或序列化。 |
||||
|
# -keep class * implements android.os.Parcelable { |
||||
|
# public static final android.os.Parcelable$Creator *; |
||||
|
# } |
||||
|
|
||||
|
# 保留 MCP 相关依赖库 |
||||
|
-keep class io.modelcontextprotocol.** { *; } |
||||
|
-keep class com.anthropic.** { *; } |
||||
|
|
||||
|
# 保留 OkHttp 相关类 |
||||
|
-keep class okhttp3.** { *; } |
||||
|
-keep class okio.** { *; } |
||||
|
|
||||
|
# 如果你使用了特定的第三方库,并且这些库需要特定的 ProGuard 规则,请在此处添加。 |
||||
|
# 例如,OkHttp/Retrofit, Gson, Glide 等通常需要保留模型类或特定方法。 |
||||
|
# 查看库的官方文档通常会提供所需的 ProGuard 规则。 |
||||
|
|
||||
|
# -keepclasseswithmembers class * { |
||||
|
# @android.webkit.JavascriptInterface <methods>; |
||||
|
# } |
||||
|
|
||||
|
# -keepattributes Signature |
||||
|
# -keepattributes *Annotation* |
||||
|
|
||||
|
# 如果你有自定义的原生插件,需要保留它们的类。 |
||||
|
# !!! 将 com.yourcompany.yourplugin.** 替换为你的插件的实际包名 !!! |
||||
|
# -keep class com.yourcompany.yourplugin.** { *; } |
||||
|
|
||||
|
# 保留枚举类,防止 switch 语句因混淆出错 |
||||
|
-keepclassmembers enum * { |
||||
|
public static **[] values(); |
||||
|
public static ** valueOf(java.lang.String); |
||||
|
} |
||||
|
|
||||
|
# 保留 R 类及其内部类,防止资源访问出错 |
||||
|
-keep class **.R$* { *; } |
||||
|
|
||||
|
# 保留 native 方法 |
||||
|
-keepclasseswithmembernames class * { |
||||
|
native <methods>; |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
storePassword=yimai_eaimar1122 |
||||
|
keyPassword=yimai_eaimar1122 |
||||
|
keyAlias=eaimar |
||||
|
storeFile=sign/eaimar.keystore |
||||
|
|
||||
|
|
||||
|
# MD5 07:3E:04:37:4D:0B:C2:7A:A7:DC:33:A0:6E:8C:08:CA |
||||
|
# SHA1 7C:4F:C6:2B:E5:B2:BC:5A:CB:30:DA:03:E3:CA:1A:00:F6:E3:A5:41 |
||||
|
# SHA256 44:41:E5:05:02:81:7E:6D:A1:B6:55:D8:2F:0D:B8:51:9B:F7:67:C3:33:65:35:F4:32:38:F9:C8:14:89:00:64 |
||||
|
# 公钥 24530696301395375919563393278066326007446934316660892176275416398340283109169195507263881320626083624969521312488884026257416528201144343730134714194063946324231679348705674394222445953519321358508742668928883457587496780325939337237867206468223139467797610758279245727661035400854264170911613957052856920151931760124241936992312636853354084771867201123929674809120813727834750033391729508797967210118449997339392146056063492805157692250413815879649405355201386251329870638240486310320693478258586362148980863435658045424690450494987908286055811654705667160237691756259350928613949331855852852146959540130564050790727 |
||||
@ -0,0 +1,7 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<!-- The INTERNET permission is required for development. Specifically, |
||||
|
the Flutter tool needs it to communicate with the running application |
||||
|
to allow setting breakpoints, to provide hot reload, etc. |
||||
|
--> |
||||
|
<uses-permission android:name="android.permission.INTERNET"/> |
||||
|
</manifest> |
||||
@ -0,0 +1,202 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:tools="http://schemas.android.com/tools"> |
||||
|
<!-- 添加必要的权限 --> |
||||
|
<uses-permission android:name="android.permission.INTERNET"/> |
||||
|
<uses-permission android:name="android.permission.GET_TASKS" /> |
||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> |
||||
|
<!-- Android 13+ 前台服务特定类型权限 --> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/> |
||||
|
<!-- Android 14+ 蓝牙设备连接前台服务权限 --> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/> |
||||
|
<!-- Android 15+ 数据同步前台服务权限 --> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> |
||||
|
<!-- Android 14+ 麦克风前台服务权限 --> |
||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> |
||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/> |
||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> |
||||
|
<uses-permission android:name="android.permission.BLUETOOTH"/> |
||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> |
||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> |
||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> |
||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> |
||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> |
||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> |
||||
|
<!-- Android 13+ 通知权限 --> |
||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> |
||||
|
<!-- 添加存储权限 --> |
||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" |
||||
|
android:maxSdkVersion="29" /> |
||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" |
||||
|
android:maxSdkVersion="32" /> |
||||
|
<!-- 添加Android 11+的存储权限 --> |
||||
|
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" |
||||
|
tools:ignore="ScopedStorage" /> --> |
||||
|
<!-- Android 13+ 媒体权限 --> |
||||
|
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> |
||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> |
||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> --> |
||||
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> |
||||
|
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 --> |
||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> |
||||
|
<!-- 添加发送短信权限 --> |
||||
|
<!-- <uses-permission android:name="android.permission.SEND_SMS" /> --> |
||||
|
<!-- 添加查询应用包权限 --> |
||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" |
||||
|
tools:ignore="QueryAllPackagesPermission" /> |
||||
|
<!--允许获取网络状态,用于网络定位(无gps情况下的定位),若需网络定位功能则必选--> |
||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |
||||
|
<!--允许获取wifi网络信息,用于网络定位(无gps情况下的定位),若需网络定位功能则必选--> |
||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> |
||||
|
<!--允许获取wifi状态改变,用于网络定位(无gps情况下的定位),若需网络定位功能则必选--> |
||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> |
||||
|
<!--后台获取位置信息,若需后台定位则必选--> |
||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> |
||||
|
<!--用于申请调用A-GPS模块,卫星定位加速--> |
||||
|
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /> |
||||
|
<!--用于申请调用精确闹钟权限--> |
||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> |
||||
|
<!--允许写设备缓存,用于问题排查--> |
||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" /> |
||||
|
<uses-permission android:name="android.permission.VIBRATE"/> |
||||
|
<uses-permission |
||||
|
android:name="android.permission.RECEIVE_BOOT_COMPLETED" |
||||
|
tools:node="remove" /> |
||||
|
|
||||
|
<application |
||||
|
android:label="EAIMAR" |
||||
|
android:name="${applicationName}" |
||||
|
android:icon="@mipmap/ic_launcher" |
||||
|
|
||||
|
android:networkSecurityConfig="@xml/network_security_config" |
||||
|
android:usesCleartextTraffic="true" |
||||
|
android:extractNativeLibs="true" |
||||
|
android:enableOnBackInvokedCallback="true" |
||||
|
tools:replace="android:label"> |
||||
|
|
||||
|
<!-- 支持16KB内存页面大小,满足Android 15要求 --> |
||||
|
<property android:name="android.allow_16kb_memory_page" android:value="true" /> |
||||
|
|
||||
|
<receiver |
||||
|
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver" |
||||
|
tools:node="remove" /> |
||||
|
|
||||
|
<!-- ============ 自启动 / 关联启动收敛(应用商店合规) ============ --> |
||||
|
<!-- 下面这几个组件都是「系统扫到已绑定的蓝牙设备 → 唤醒本应用」链路上的, |
||||
|
插件自带的清单把它们声明成 exported=true,等于任何第三方应用都能把本应用 |
||||
|
拉起来(关联启动)。它们实际只接收本应用自己创建的 PendingIntent, |
||||
|
同进程投递不需要 exported,这里统一收成 false。 --> |
||||
|
<receiver |
||||
|
android:name="com.recorder.holder.BleWakeReceiver" |
||||
|
android:exported="false" |
||||
|
tools:replace="android:exported" /> |
||||
|
<service |
||||
|
android:name="com.recorder.holder.BleMediaService" |
||||
|
android:exported="false" |
||||
|
tools:replace="android:exported" /> |
||||
|
<receiver |
||||
|
android:name="com.yunqiinnovation.ble_service.BleWakeReceiver" |
||||
|
android:exported="false" |
||||
|
tools:replace="android:exported" /> |
||||
|
|
||||
|
<!-- WorkManager 的诊断广播口子:exported=true,adb / 其他应用可据此拉起进程, |
||||
|
本项目没用到诊断能力,直接摘掉 --> |
||||
|
<receiver |
||||
|
android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver" |
||||
|
tools:node="remove" /> |
||||
|
|
||||
|
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> |
||||
|
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/> |
||||
|
|
||||
|
|
||||
|
<activity |
||||
|
android:name=".MainActivity" |
||||
|
android:exported="true" |
||||
|
android:launchMode="singleTask" |
||||
|
android:theme="@style/LaunchTheme" |
||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" |
||||
|
android:screenOrientation="portrait" |
||||
|
android:hardwareAccelerated="true" |
||||
|
android:windowSoftInputMode="adjustResize"> |
||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as |
||||
|
the Android process has started. This theme is visible to the user |
||||
|
while the Flutter UI initializes. After that, this theme continues |
||||
|
to determine the Window background behind the Flutter UI. --> |
||||
|
<meta-data |
||||
|
android:name="io.flutter.embedding.android.NormalTheme" |
||||
|
android:resource="@style/NormalTheme" |
||||
|
/> |
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.MAIN"/> |
||||
|
<category android:name="android.intent.category.LAUNCHER"/> |
||||
|
</intent-filter> |
||||
|
|
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.VIEW"/> |
||||
|
<category android:name="android.intent.category.DEFAULT"/> |
||||
|
<category android:name="android.intent.category.BROWSABLE"/> |
||||
|
<data android:scheme="eaimar"/> |
||||
|
</intent-filter> |
||||
|
|
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.VIEW" /> |
||||
|
<category android:name="android.intent.category.DEFAULT" /> |
||||
|
<category android:name="android.intent.category.BROWSABLE" /> |
||||
|
<data android:scheme="content" android:mimeType="audio/*" /> |
||||
|
<data android:scheme="file" android:mimeType="audio/*" /> |
||||
|
</intent-filter> |
||||
|
|
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.SEND" /> |
||||
|
<category android:name="android.intent.category.DEFAULT" /> |
||||
|
<data android:mimeType="audio/*" /> |
||||
|
</intent-filter> |
||||
|
|
||||
|
<!-- PayPal 网页回跳的深链:deepvoiceclient://paypal/return 与 /cancel --> |
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.VIEW"/> |
||||
|
<category android:name="android.intent.category.DEFAULT"/> |
||||
|
<category android:name="android.intent.category.BROWSABLE"/> |
||||
|
<data android:scheme="deepvoiceclient" android:host="paypal" android:pathPrefix="/return" /> |
||||
|
</intent-filter> |
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.VIEW"/> |
||||
|
<category android:name="android.intent.category.DEFAULT"/> |
||||
|
<category android:name="android.intent.category.BROWSABLE"/> |
||||
|
<data android:scheme="deepvoiceclient" android:host="paypal" android:pathPrefix="/cancel" /> |
||||
|
</intent-filter> |
||||
|
</activity> |
||||
|
<!-- Don't delete the meta-data below. |
||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> |
||||
|
<meta-data |
||||
|
android:name="flutterEmbedding" |
||||
|
android:value="2" /> |
||||
|
|
||||
|
<meta-data |
||||
|
android:name="com.amap.api.v2.apikey" |
||||
|
android:value="78e7946dd602778df82c6ae143a88184"/> |
||||
|
|
||||
|
<service android:name="com.amap.api.location.APSService"></service> |
||||
|
</application> |
||||
|
<!-- Required to query activities that can process text, see: |
||||
|
https://developer.android.com/training/package-visibility and |
||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. |
||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> |
||||
|
<queries> |
||||
|
<intent> |
||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/> |
||||
|
<data android:mimeType="text/plain"/> |
||||
|
</intent> |
||||
|
</queries> |
||||
|
|
||||
|
<queries> |
||||
|
<!-- 声明百度地图包名 --> |
||||
|
<package android:name="com.baidu.BaiduMap" /> |
||||
|
<!-- 声明要处理的URL Scheme --> |
||||
|
<intent> |
||||
|
<action android:name="android.intent.action.VIEW" /> |
||||
|
<data android:scheme="baidumap" /> |
||||
|
</intent> |
||||
|
</queries> |
||||
|
</manifest> |
||||
@ -0,0 +1,541 @@ |
|||||
|
package com.yimai.eaimar |
||||
|
|
||||
|
import android.app.PendingIntent |
||||
|
import android.bluetooth.le.ScanFilter |
||||
|
import android.bluetooth.le.ScanSettings |
||||
|
import android.content.Intent |
||||
|
import android.os.Build |
||||
|
import android.os.Bundle |
||||
|
import io.flutter.embedding.engine.FlutterEngine |
||||
|
import io.flutter.plugins.GeneratedPluginRegistrant |
||||
|
import io.flutter.embedding.android.FlutterActivity |
||||
|
import android.content.Context |
||||
|
import android.os.Handler |
||||
|
import android.os.Looper |
||||
|
import android.util.Log |
||||
|
import io.flutter.embedding.engine.FlutterEngineCache |
||||
|
import io.flutter.plugin.common.MethodChannel // 添加这行导入 |
||||
|
import android.net.Uri |
||||
|
import android.provider.OpenableColumns |
||||
|
import java.io.File |
||||
|
import java.io.FileOutputStream |
||||
|
import org.json.JSONObject |
||||
|
|
||||
|
class MainActivity: FlutterActivity() { |
||||
|
private val TAG = "MainActivity" |
||||
|
private var pendingDeepLink: Intent? = null |
||||
|
private var pendingNavigationIntent: Intent? = null |
||||
|
private var pendingShareIntent: Intent? = null |
||||
|
private var pendingSharedFilePath: String? = null |
||||
|
private var pendingDeepLinkUrl: String? = null |
||||
|
|
||||
|
private val EXTRA_NAVIGATE_TO = "com.yimai.eaimar.extra.NAVIGATE_TO" |
||||
|
private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord" |
||||
|
private val ROUTE_MEETING_RECORD = "/meeting/record" |
||||
|
private val DEEPLINK_CHANNEL = "com.yimai.eaimar/deeplink" |
||||
|
private val DELIVERY_RETRY_DELAY_MS = 300L |
||||
|
private val MAX_DELIVERY_RETRIES = 12 |
||||
|
private val NATIVE_LOCALIZATION_CHANNEL = "com.yimai.eaimar/native_localization" |
||||
|
private val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" |
||||
|
private val NATIVE_LOCALIZATION_PREF_KEY_LOCALE = "locale" |
||||
|
private val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
Log.d(TAG, "🚀 MainActivity onCreate 开始") |
||||
|
super.onCreate(savedInstanceState) |
||||
|
|
||||
|
// 检查 Intent 信息 |
||||
|
intent?.let { |
||||
|
Log.d(TAG, "Intent action: ${it.action}") |
||||
|
Log.d(TAG, "Intent data: ${it.data}") |
||||
|
Log.d(TAG, "Intent extras: ${it.extras}") |
||||
|
} |
||||
|
|
||||
|
intent?.let { |
||||
|
if (it.action == Intent.ACTION_SEND) { |
||||
|
pendingShareIntent = it |
||||
|
Log.d(TAG, "📎 暂存分享文件意图,等待Flutter引擎准备完成") |
||||
|
} else if (it.action == Intent.ACTION_VIEW && it.data != null) { |
||||
|
val uri = it.data |
||||
|
if (uri != null && isLikelyAudio(it, uri)) { |
||||
|
pendingShareIntent = it |
||||
|
Log.d(TAG, "📎 暂存打开文件意图,等待Flutter引擎准备完成") |
||||
|
} else { |
||||
|
pendingDeepLink = it |
||||
|
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (intent?.hasExtra(EXTRA_NAVIGATE_TO) == true) { |
||||
|
pendingNavigationIntent = intent |
||||
|
Log.d(TAG, "🧭 暂存导航意图,等待Flutter引擎准备完成") |
||||
|
} |
||||
|
|
||||
|
// 设置日志级别,减少系统级日志 |
||||
|
suppressSystemLogs() |
||||
|
Log.d(TAG, "✅ MainActivity onCreate 完成") |
||||
|
} |
||||
|
|
||||
|
// 添加 onNewIntent 方法处理深链接 |
||||
|
override fun onNewIntent(intent: Intent) { |
||||
|
Log.d(TAG, "🔗 onNewIntent 被调用") |
||||
|
super.onNewIntent(intent) |
||||
|
setIntent(intent) // 更新当前 Intent |
||||
|
|
||||
|
// 检查新的 Intent 信息 |
||||
|
Log.d(TAG, "New Intent action: ${intent.action}") |
||||
|
Log.d(TAG, "New Intent data: ${intent.data}") |
||||
|
Log.d(TAG, "New Intent extras: ${intent.extras}") |
||||
|
|
||||
|
intent.let { |
||||
|
if (it.action == Intent.ACTION_SEND) { |
||||
|
pendingShareIntent = it |
||||
|
Log.d(TAG, "📎 暂存分享文件意图,等待Flutter引擎准备完成") |
||||
|
} else if (it.action == Intent.ACTION_VIEW && it.data != null) { |
||||
|
val uri = it.data |
||||
|
if (uri != null && isLikelyAudio(it, uri)) { |
||||
|
pendingShareIntent = it |
||||
|
Log.d(TAG, "📎 暂存打开文件意图,等待Flutter引擎准备完成") |
||||
|
} else { |
||||
|
pendingDeepLink = it |
||||
|
Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (intent.hasExtra(EXTRA_NAVIGATE_TO)) { |
||||
|
pendingNavigationIntent = intent |
||||
|
Log.d(TAG, "🧭 暂存导航意图,等待Flutter引擎准备完成") |
||||
|
} |
||||
|
|
||||
|
deliverOrQueueIntent(intent) |
||||
|
} |
||||
|
|
||||
|
override fun provideFlutterEngine(context: Context): FlutterEngine? { |
||||
|
Log.d(TAG, "🔍 provideFlutterEngine 被调用") |
||||
|
val engine = FlutterEngineCache.getInstance().get("main_engine") |
||||
|
if (engine == null) { |
||||
|
Log.w(TAG, "❌ FlutterEngine 缓存为空,这可能导致问题") |
||||
|
// 如果缓存为空,让 Flutter 框架创建默认 Engine |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
// 检查 Engine 状态 - 使用更兼容的方式 |
||||
|
try { |
||||
|
// 检查 Dart 执行器是否正在运行 |
||||
|
val isExecutorRunning = engine.dartExecutor.isExecutingDart |
||||
|
Log.d(TAG, "FlutterEngine Dart 执行状态: $isExecutorRunning") |
||||
|
|
||||
|
// 如果需要,可以检查其他状态 |
||||
|
if (!isExecutorRunning) { |
||||
|
Log.w(TAG, "⚠️ FlutterEngine Dart 执行器未运行,可能需要重新初始化") |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.w(TAG, "❌ 检查 FlutterEngine 状态时出错", e) |
||||
|
} |
||||
|
|
||||
|
Log.d(TAG, "✅ 使用缓存的 FlutterEngine") |
||||
|
return engine |
||||
|
} |
||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { |
||||
|
Log.d(TAG, "🔧 configureFlutterEngine 开始") |
||||
|
|
||||
|
// 检查插件是否已经注册,避免重复注册 |
||||
|
val pluginRegistry = flutterEngine.plugins |
||||
|
|
||||
|
// 只有在插件未注册时才调用 GeneratedPluginRegistrant |
||||
|
@Suppress("UNCHECKED_CAST") |
||||
|
val pathProviderClass = try { |
||||
|
Class.forName("io.flutter.plugins.pathprovider.PathProviderPlugin") |
||||
|
as Class<out io.flutter.embedding.engine.plugins.FlutterPlugin> |
||||
|
} catch (e: ClassNotFoundException) { |
||||
|
null |
||||
|
} |
||||
|
|
||||
|
if (pathProviderClass == null || !pluginRegistry.has(pathProviderClass)) { |
||||
|
Log.d(TAG, "📦 注册 Flutter 插件") |
||||
|
super.configureFlutterEngine(flutterEngine) |
||||
|
GeneratedPluginRegistrant.registerWith(flutterEngine) |
||||
|
} else { |
||||
|
Log.d(TAG, "⚠️ 插件已注册,跳过重复注册") |
||||
|
} |
||||
|
|
||||
|
setupNativeLocalizationChannel(flutterEngine) |
||||
|
|
||||
|
// Flutter引擎配置完成后,处理待处理的深链接 |
||||
|
Handler(Looper.getMainLooper()).postDelayed({ |
||||
|
pendingShareIntent?.let { intent -> |
||||
|
Log.d(TAG, "📎 处理待处理的分享/打开文件意图") |
||||
|
deliverOrQueueIntent(intent) |
||||
|
pendingShareIntent = null |
||||
|
} |
||||
|
|
||||
|
pendingDeepLink?.let { intent -> |
||||
|
Log.d(TAG, "🔗 处理待处理的深链接") |
||||
|
deliverOrQueueIntent(intent) |
||||
|
pendingDeepLink = null |
||||
|
} |
||||
|
|
||||
|
pendingNavigationIntent?.let { intent -> |
||||
|
Log.d(TAG, "🧭 处理待处理的导航意图") |
||||
|
deliverOrQueueIntent(intent) |
||||
|
pendingNavigationIntent = null |
||||
|
} |
||||
|
}, 1000) // 延迟1秒确保Flutter完全准备好 |
||||
|
|
||||
|
Log.d(TAG, "✅ configureFlutterEngine 完成") |
||||
|
} |
||||
|
|
||||
|
private fun setupNativeLocalizationChannel(flutterEngine: FlutterEngine) { |
||||
|
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NATIVE_LOCALIZATION_CHANNEL) |
||||
|
channel.setMethodCallHandler { call, result -> |
||||
|
when (call.method) { |
||||
|
"updateNativeStrings" -> { |
||||
|
try { |
||||
|
val args = call.arguments as? Map<*, *> |
||||
|
val locale = args?.get("locale") as? String |
||||
|
val strings = args?.get("strings") as? Map<*, *> |
||||
|
|
||||
|
val obj = JSONObject() |
||||
|
strings?.forEach { (k, v) -> |
||||
|
val key = k as? String ?: return@forEach |
||||
|
val value = v as? String ?: return@forEach |
||||
|
obj.put(key, value) |
||||
|
} |
||||
|
|
||||
|
val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) |
||||
|
prefs.edit() |
||||
|
.putString(NATIVE_LOCALIZATION_PREF_KEY_LOCALE, locale) |
||||
|
.putString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, obj.toString()) |
||||
|
.apply() |
||||
|
|
||||
|
result.success(null) |
||||
|
} catch (e: Exception) { |
||||
|
result.error("NATIVE_LOCALIZATION_ERROR", e.message, null) |
||||
|
} |
||||
|
} |
||||
|
else -> result.notImplemented() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun deliverOrQueueIntent(intent: Intent?) { |
||||
|
intent ?: return |
||||
|
|
||||
|
val navigateTo = intent.getStringExtra(EXTRA_NAVIGATE_TO) |
||||
|
if (navigateTo == NAVIGATE_TO_MEETING_RECORD) { |
||||
|
pushRouteToFlutter(ROUTE_MEETING_RECORD) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (intent.action == Intent.ACTION_SEND || |
||||
|
intent.action == Intent.ACTION_SEND_MULTIPLE || |
||||
|
intent.action == Intent.ACTION_VIEW |
||||
|
) { |
||||
|
try { |
||||
|
val path = extractAudioPathFromIntent(intent) |
||||
|
if (path != null) { |
||||
|
pendingSharedFilePath = path |
||||
|
deliverPendingSharedFile() |
||||
|
return |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ 处理分享/打开文件失败", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (intent.action == Intent.ACTION_VIEW && intent.data != null) { |
||||
|
val uri = intent.data |
||||
|
Log.d(TAG, "🔗 处理深链接: $uri") |
||||
|
pendingDeepLinkUrl = uri.toString() |
||||
|
deliverPendingDeepLink() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
Log.d(TAG, "🔗 Intent 不是深链接或数据为空") |
||||
|
} |
||||
|
|
||||
|
private fun getEngineForMessaging(): FlutterEngine? { |
||||
|
return flutterEngine ?: FlutterEngineCache.getInstance().get("main_engine") |
||||
|
} |
||||
|
|
||||
|
private fun deliverPendingSharedFile(attempt: Int = 0) { |
||||
|
val filePath = pendingSharedFilePath ?: return |
||||
|
val engine = getEngineForMessaging() |
||||
|
val messenger = engine?.dartExecutor?.binaryMessenger |
||||
|
if (messenger == null) { |
||||
|
if (attempt < MAX_DELIVERY_RETRIES) { |
||||
|
Handler(Looper.getMainLooper()).postDelayed( |
||||
|
{ deliverPendingSharedFile(attempt + 1) }, |
||||
|
DELIVERY_RETRY_DELAY_MS |
||||
|
) |
||||
|
} else { |
||||
|
Log.w(TAG, "❌ FlutterEngine 未就绪,放弃发送分享文件:$filePath") |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
val channel = MethodChannel(messenger, DEEPLINK_CHANNEL) |
||||
|
channel.invokeMethod( |
||||
|
"handleSharedFile", |
||||
|
mapOf("filePath" to filePath), |
||||
|
object : MethodChannel.Result { |
||||
|
override fun success(result: Any?) { |
||||
|
pendingSharedFilePath = null |
||||
|
} |
||||
|
|
||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { |
||||
|
Log.e(TAG, "❌ Flutter 处理分享文件返回错误:$errorCode, $errorMessage") |
||||
|
} |
||||
|
|
||||
|
override fun notImplemented() { |
||||
|
if (attempt < MAX_DELIVERY_RETRIES) { |
||||
|
Handler(Looper.getMainLooper()).postDelayed( |
||||
|
{ deliverPendingSharedFile(attempt + 1) }, |
||||
|
DELIVERY_RETRY_DELAY_MS |
||||
|
) |
||||
|
} else { |
||||
|
Log.w(TAG, "❌ Flutter 未注册分享处理器,放弃发送分享文件:$filePath") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
private fun deliverPendingDeepLink(attempt: Int = 0) { |
||||
|
val url = pendingDeepLinkUrl ?: return |
||||
|
val engine = getEngineForMessaging() |
||||
|
val messenger = engine?.dartExecutor?.binaryMessenger |
||||
|
if (messenger == null) { |
||||
|
if (attempt < MAX_DELIVERY_RETRIES) { |
||||
|
Handler(Looper.getMainLooper()).postDelayed( |
||||
|
{ deliverPendingDeepLink(attempt + 1) }, |
||||
|
DELIVERY_RETRY_DELAY_MS |
||||
|
) |
||||
|
} else { |
||||
|
Log.w(TAG, "❌ FlutterEngine 未就绪,放弃发送深链接:$url") |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
val channel = MethodChannel(messenger, DEEPLINK_CHANNEL) |
||||
|
val arguments = buildUrlArguments(url) |
||||
|
channel.invokeMethod( |
||||
|
"handleDeepLink", |
||||
|
arguments, |
||||
|
object : MethodChannel.Result { |
||||
|
override fun success(result: Any?) { |
||||
|
pendingDeepLinkUrl = null |
||||
|
} |
||||
|
|
||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { |
||||
|
Log.e(TAG, "❌ Flutter 处理深链接返回错误:$errorCode, $errorMessage") |
||||
|
} |
||||
|
|
||||
|
override fun notImplemented() { |
||||
|
if (attempt < MAX_DELIVERY_RETRIES) { |
||||
|
Handler(Looper.getMainLooper()).postDelayed( |
||||
|
{ deliverPendingDeepLink(attempt + 1) }, |
||||
|
DELIVERY_RETRY_DELAY_MS |
||||
|
) |
||||
|
} else { |
||||
|
Log.w(TAG, "❌ Flutter 未注册深链接处理器,放弃发送深链接:$url") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
private fun extractAudioPathFromIntent(intent: Intent): String? { |
||||
|
val uri: Uri? = when (intent.action) { |
||||
|
Intent.ACTION_SEND -> { |
||||
|
val extraStream = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) |
||||
|
extraStream ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri |
||||
|
} |
||||
|
Intent.ACTION_SEND_MULTIPLE -> { |
||||
|
val items = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM) |
||||
|
items?.firstOrNull() ?: intent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri |
||||
|
} |
||||
|
Intent.ACTION_VIEW -> intent.data |
||||
|
else -> null |
||||
|
} |
||||
|
if (uri == null) return null |
||||
|
if (!isLikelyAudio(intent, uri)) return null |
||||
|
return copyUriToCache(uri) |
||||
|
} |
||||
|
|
||||
|
private fun isLikelyAudio(intent: Intent, uri: Uri): Boolean { |
||||
|
val type = intent.type ?: "" |
||||
|
if (type.startsWith("audio/")) return true |
||||
|
val scheme = uri.scheme ?: "" |
||||
|
if (scheme == "content" || scheme == "file") return true |
||||
|
val path = uri.toString().lowercase() |
||||
|
return path.endsWith(".wav") || path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".aac") |
||||
|
} |
||||
|
|
||||
|
private fun copyUriToCache(uri: Uri): String? { |
||||
|
return try { |
||||
|
val resolver = applicationContext.contentResolver |
||||
|
val cursor = resolver.query(uri, null, null, null, null) |
||||
|
var displayName = "shared_audio" |
||||
|
cursor?.use { |
||||
|
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) |
||||
|
if (nameIndex >= 0 && it.moveToFirst()) { |
||||
|
displayName = it.getString(nameIndex) ?: displayName |
||||
|
} |
||||
|
} |
||||
|
val ext = when { |
||||
|
displayName.endsWith(".wav", true) -> "wav" |
||||
|
displayName.endsWith(".mp3", true) -> "mp3" |
||||
|
displayName.endsWith(".m4a", true) -> "m4a" |
||||
|
displayName.endsWith(".aac", true) -> "aac" |
||||
|
else -> displayName.substringAfterLast('.', "audio") |
||||
|
} |
||||
|
val sharedDir = File(applicationContext.filesDir, "shared_audio") |
||||
|
if (!sharedDir.exists()) sharedDir.mkdirs() |
||||
|
val outFile = File(sharedDir, "audio_${System.currentTimeMillis()}.$ext") |
||||
|
val inStream = resolver.openInputStream(uri) |
||||
|
if (inStream == null) { |
||||
|
outFile.delete() |
||||
|
return null |
||||
|
} |
||||
|
inStream.use { |
||||
|
FileOutputStream(outFile).use { out -> |
||||
|
val buffer = ByteArray(8192) |
||||
|
var len: Int |
||||
|
while (true) { |
||||
|
len = it.read(buffer) |
||||
|
if (len <= 0) break |
||||
|
out.write(buffer, 0, len) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (!outFile.exists() || outFile.length() <= 0) { |
||||
|
outFile.delete() |
||||
|
return null |
||||
|
} |
||||
|
outFile.absolutePath |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "复制分享文件到缓存失败: ${e.message}", e) |
||||
|
null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun pushRouteToFlutter(route: String) { |
||||
|
try { |
||||
|
val engine = flutterEngine |
||||
|
if (engine == null) { |
||||
|
Log.w(TAG, "❌ FlutterEngine 为空,无法 pushRoute") |
||||
|
return |
||||
|
} |
||||
|
Handler(Looper.getMainLooper()).post { |
||||
|
try { |
||||
|
engine.navigationChannel.pushRoute(route) |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ pushRoute 失败", e) |
||||
|
} |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ pushRouteToFlutter 出错", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun buildUrlArguments(url: String): Map<String, Any?> { |
||||
|
return try { |
||||
|
val uri = android.net.Uri.parse(url) |
||||
|
mapOf( |
||||
|
"url" to url, |
||||
|
"scheme" to uri.scheme, |
||||
|
"host" to uri.host, |
||||
|
"path" to uri.path, |
||||
|
"query" to uri.query, |
||||
|
"queryParameters" to uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } |
||||
|
) |
||||
|
} catch (e: Exception) { |
||||
|
mapOf("url" to url) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onDestroy() { |
||||
|
super.onDestroy() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 抑制系统级组件的日志输出 |
||||
|
*/ |
||||
|
private fun suppressSystemLogs() { |
||||
|
// 更全面的系统标签列表 |
||||
|
val systemTags = arrayOf( |
||||
|
"MediaCodec", |
||||
|
"CCodec", |
||||
|
"CCodecBufferChannel", |
||||
|
"OpusManager", |
||||
|
"MediaCodecList", |
||||
|
"MediaPlayer", |
||||
|
"AudioTrack", |
||||
|
"AudioManager", |
||||
|
"AudioFlinger", |
||||
|
"AudioService", |
||||
|
"AudioEffect", |
||||
|
"AppOpsManager", |
||||
|
"AudioRecord", |
||||
|
"OpenGLRenderer", |
||||
|
"Choreographer", |
||||
|
"InputMethodManager", |
||||
|
"ViewRootImpl", |
||||
|
"Surface", |
||||
|
"BufferQueueProducer", |
||||
|
"ActivityManager", |
||||
|
"WindowManager", |
||||
|
"SensorManager" |
||||
|
) |
||||
|
|
||||
|
// 尝试多种方式设置日志级别 |
||||
|
systemTags.forEach { tag -> |
||||
|
try { |
||||
|
// 方式1:标准属性设置 |
||||
|
System.setProperty("log.tag.$tag", "ERROR") |
||||
|
|
||||
|
// 方式2:尝试使用反射设置(针对某些特殊标签) |
||||
|
setLogLevelViaReflection(tag, Log.ERROR) |
||||
|
} catch (e: Exception) { |
||||
|
// 忽略设置失败的情况 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 方式3:尝试设置全局日志级别 |
||||
|
try { |
||||
|
// 设置更高的阈值,减少verbose和debug日志 |
||||
|
System.setProperty("log.tag", "WARN") |
||||
|
} catch (e: Exception) { |
||||
|
// 忽略 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 通过反射尝试设置日志级别 |
||||
|
*/ |
||||
|
private fun setLogLevelViaReflection(tag: String, level: Int) { |
||||
|
try { |
||||
|
val logClass = Class.forName("android.util.Log") |
||||
|
val isLoggableMethod = logClass.getDeclaredMethod( |
||||
|
"isLoggable", |
||||
|
String::class.java, |
||||
|
Int::class.javaPrimitiveType |
||||
|
) |
||||
|
isLoggableMethod.isAccessible = true |
||||
|
|
||||
|
// 某些系统可能支持直接设置 |
||||
|
val setLogLevelMethod = logClass.getDeclaredMethod( |
||||
|
"setLogLevel", |
||||
|
String::class.java, |
||||
|
Int::class.javaPrimitiveType |
||||
|
) |
||||
|
setLogLevelMethod.isAccessible = true |
||||
|
setLogLevelMethod.invoke(null, tag, level) |
||||
|
} catch (e: Exception) { |
||||
|
// 反射方法可能不存在,忽略 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
package com.yimai.eaimar |
||||
|
|
||||
|
import android.app.ActivityManager |
||||
|
import android.content.Context |
||||
|
import android.util.Log |
||||
|
import io.flutter.app.FlutterApplication |
||||
|
import io.flutter.embedding.engine.FlutterEngine |
||||
|
import io.flutter.embedding.engine.FlutterEngineCache |
||||
|
import io.flutter.embedding.engine.dart.DartExecutor |
||||
|
import io.flutter.plugins.GeneratedPluginRegistrant |
||||
|
import java.io.OutputStream |
||||
|
import java.io.PrintStream |
||||
|
|
||||
|
// Application.kt |
||||
|
class MyApp : FlutterApplication() { |
||||
|
|
||||
|
companion object { |
||||
|
private const val TAG = "MyApp" |
||||
|
private const val ENGINE_ID = "main_engine" |
||||
|
|
||||
|
// 标记 Engine 是否已经执行过 Dart 入口点 |
||||
|
@Volatile |
||||
|
private var isDartEntrypointExecuted = false |
||||
|
|
||||
|
// 记录应用启动时间,用于判断是否为新启动 |
||||
|
@Volatile |
||||
|
private var appStartTime = 0L |
||||
|
} |
||||
|
|
||||
|
override fun onCreate() { |
||||
|
super.onCreate() |
||||
|
appStartTime = System.currentTimeMillis() |
||||
|
Log.d(TAG, "MyApp onCreate - 进程启动") |
||||
|
|
||||
|
// 屏蔽高德定位 SDK(CgiManager) 的 NumberFormatException 噪音日志 |
||||
|
installAmapStderrFilter() |
||||
|
|
||||
|
val cache = FlutterEngineCache.getInstance() |
||||
|
val existingEngine = cache.get(ENGINE_ID) |
||||
|
|
||||
|
if (existingEngine == null) { |
||||
|
Log.d(TAG, "创建新的 FlutterEngine") |
||||
|
createAndCacheEngine(cache) |
||||
|
} else { |
||||
|
Log.d(TAG, "FlutterEngine 已存在,检查状态") |
||||
|
// 检查现有 Engine 的状态 |
||||
|
try { |
||||
|
val isExecuting = existingEngine.dartExecutor.isExecutingDart |
||||
|
Log.d(TAG, "Engine Dart执行状态: $isExecuting") |
||||
|
|
||||
|
if (!isExecuting && isMainProcess()) { |
||||
|
Log.d(TAG, "Engine 存在但Dart未执行,重新执行") |
||||
|
existingEngine.dartExecutor.executeDartEntrypoint( |
||||
|
DartExecutor.DartEntrypoint.createDefault() |
||||
|
) |
||||
|
isDartEntrypointExecuted = true |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "检查或重新执行 Dart 入口点失败", e) |
||||
|
// 如果失败,重新创建 Engine |
||||
|
cache.remove(ENGINE_ID) |
||||
|
createAndCacheEngine(cache) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun createAndCacheEngine(cache: FlutterEngineCache) { |
||||
|
// 在主进程中总是创建并执行Dart入口点 |
||||
|
if (isMainProcess()) { |
||||
|
Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart") |
||||
|
val flutterEngine = FlutterEngine(this) |
||||
|
|
||||
|
// 注册插件(只在这里注册一次) |
||||
|
Log.d(TAG, "📦 注册 Flutter 插件") |
||||
|
GeneratedPluginRegistrant.registerWith(flutterEngine) |
||||
|
|
||||
|
try { |
||||
|
Log.d(TAG, "🚀 开始执行 Dart 入口点") |
||||
|
flutterEngine.dartExecutor.executeDartEntrypoint( |
||||
|
DartExecutor.DartEntrypoint.createDefault() |
||||
|
) |
||||
|
isDartEntrypointExecuted = true |
||||
|
cache.put(ENGINE_ID, flutterEngine) |
||||
|
Log.d(TAG, "✅ FlutterEngine 创建并缓存成功") |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ 执行 Dart 入口点失败", e) |
||||
|
// 如果执行失败,仍然缓存 Engine,但标记为未执行 |
||||
|
isDartEntrypointExecuted = false |
||||
|
cache.put(ENGINE_ID, flutterEngine) |
||||
|
} |
||||
|
} else { |
||||
|
Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine") |
||||
|
// 非主进程只创建 Engine,不执行 Dart 入口点 |
||||
|
val flutterEngine = FlutterEngine(this) |
||||
|
cache.put(ENGINE_ID, flutterEngine) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun isMainProcess(): Boolean { |
||||
|
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager |
||||
|
val processes = am.runningAppProcesses ?: return true |
||||
|
val mainProcessName = packageName |
||||
|
val myPid = android.os.Process.myPid() |
||||
|
|
||||
|
for (process in processes) { |
||||
|
if (process.pid == myPid) { |
||||
|
return process.processName == mainProcessName |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 过滤 System.err 中来自高德 SDK 的 NumberFormatException 栈(com.autonavi.aps.amapapi / CgiManager / ApsManager)。 |
||||
|
* 仅丢弃这类已知噪音,其他 stderr 输出原样打印。 |
||||
|
*/ |
||||
|
private fun installAmapStderrFilter() { |
||||
|
val origin = System.err ?: return |
||||
|
val noiseKeywords = arrayOf( |
||||
|
"com.autonavi.aps.amapapi", |
||||
|
"CgiManager", |
||||
|
"ApsManager", |
||||
|
"onCellLocationChanged", |
||||
|
"PhoneStateListener", |
||||
|
) |
||||
|
val bufferRef = StringBuilder() |
||||
|
var suppressing = false |
||||
|
|
||||
|
val filtered = PrintStream(object : OutputStream() { |
||||
|
override fun write(b: Int) { |
||||
|
val ch = b.toChar() |
||||
|
bufferRef.append(ch) |
||||
|
if (ch == '\n') { |
||||
|
val line = bufferRef.toString() |
||||
|
bufferRef.setLength(0) |
||||
|
val trimmed = line.trimStart() |
||||
|
if (trimmed.startsWith("java.lang.NumberFormatException")) { |
||||
|
suppressing = true |
||||
|
return |
||||
|
} |
||||
|
if (suppressing) { |
||||
|
// 只要是栈帧 (at xxx) 或 caused by,就继续丢弃 |
||||
|
if (trimmed.startsWith("at ") || |
||||
|
trimmed.startsWith("Caused by") || |
||||
|
trimmed.startsWith("... ") || |
||||
|
noiseKeywords.any { trimmed.contains(it) } |
||||
|
) { |
||||
|
return |
||||
|
} |
||||
|
suppressing = false |
||||
|
} |
||||
|
origin.print(line) |
||||
|
} |
||||
|
} |
||||
|
}, true) |
||||
|
System.setErr(filtered) |
||||
|
Log.d(TAG, "已安装高德 SDK stderr 过滤器") |
||||
|
} |
||||
|
|
||||
|
private fun isAppInForeground(): Boolean { |
||||
|
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager |
||||
|
val processes = am.runningAppProcesses ?: return false |
||||
|
val pkg = packageName |
||||
|
for (p in processes) { |
||||
|
if (p.processName == pkg && |
||||
|
p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
private fun isAppBeingLaunched(): Boolean { |
||||
|
// 改进的启动检测逻辑 |
||||
|
val currentTime = System.currentTimeMillis() |
||||
|
val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动 |
||||
|
|
||||
|
if (isRecentStart) { |
||||
|
Log.d(TAG, "检测到应用新启动") |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// 备用检测方法(兼容旧版本) |
||||
|
return try { |
||||
|
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager |
||||
|
val tasks = am.getRunningTasks(1) |
||||
|
val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName |
||||
|
Log.d(TAG, "通过 RunningTasks 检测启动状态: $result") |
||||
|
result |
||||
|
} catch (e: Exception) { |
||||
|
Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e) |
||||
|
// 如果无法获取任务信息,默认认为是启动状态 |
||||
|
true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- Modify this file to customize your launch splash screen --> |
||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<item android:drawable="?android:colorBackground" /> |
||||
|
|
||||
|
<!-- You can insert your own image assets here --> |
||||
|
<!-- <item> |
||||
|
<bitmap |
||||
|
android:gravity="center" |
||||
|
android:src="@mipmap/launch_image" /> |
||||
|
</item> --> |
||||
|
</layer-list> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- Modify this file to customize your launch splash screen --> |
||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<item android:drawable="@android:color/white" /> |
||||
|
|
||||
|
<!-- You can insert your own image assets here --> |
||||
|
<!-- <item> |
||||
|
<bitmap |
||||
|
android:gravity="center" |
||||
|
android:src="@mipmap/launch_image" /> |
||||
|
</item> --> |
||||
|
</layer-list> |
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 33 KiB |
@ -0,0 +1,18 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on --> |
||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> |
||||
|
<!-- Show a splash screen on the activity. Automatically removed when |
||||
|
the Flutter engine draws its first frame --> |
||||
|
<item name="android:windowBackground">@drawable/launch_background</item> |
||||
|
</style> |
||||
|
<!-- Theme applied to the Android Window as soon as the process has started. |
||||
|
This theme determines the color of the Android Window while your |
||||
|
Flutter UI initializes, as well as behind your Flutter UI while its |
||||
|
running. |
||||
|
|
||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. --> |
||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> |
||||
|
<item name="android:windowBackground">?android:colorBackground</item> |
||||
|
</style> |
||||
|
</resources> |
||||
@ -0,0 +1,3 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
</resources> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<string name="facebook_app_id">3829022037361855</string> |
||||
|
<string name="fb_login_protocol_scheme">fb3829022037361855</string> |
||||
|
<string name="facebook_client_token">193d0a2b1757c41cfa557ea8e8f21c33</string> |
||||
|
</resources> |
||||
@ -0,0 +1,18 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> |
||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> |
||||
|
<!-- Show a splash screen on the activity. Automatically removed when |
||||
|
the Flutter engine draws its first frame --> |
||||
|
<item name="android:windowBackground">@drawable/launch_background</item> |
||||
|
</style> |
||||
|
<!-- Theme applied to the Android Window as soon as the process has started. |
||||
|
This theme determines the color of the Android Window while your |
||||
|
Flutter UI initializes, as well as behind your Flutter UI while its |
||||
|
running. |
||||
|
|
||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. --> |
||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> |
||||
|
<item name="android:windowBackground">?android:colorBackground</item> |
||||
|
</style> |
||||
|
</resources> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<network-security-config> |
||||
|
<base-config cleartextTrafficPermitted="true"> |
||||
|
<trust-anchors> |
||||
|
<certificates src="system" /> |
||||
|
<certificates src="user" /> |
||||
|
</trust-anchors> |
||||
|
</base-config> |
||||
|
<domain-config cleartextTrafficPermitted="true"> |
||||
|
<domain includeSubdomains="true">127.0.0.1</domain> |
||||
|
<domain includeSubdomains="true">localhost</domain> |
||||
|
</domain-config> |
||||
|
</network-security-config> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<!-- The INTERNET permission is required for development. Specifically, |
||||
|
the Flutter tool needs it to communicate with the running application |
||||
|
to allow setting breakpoints, to provide hot reload, etc. |
||||
|
--> |
||||
|
<uses-permission android:name="android.permission.INTERNET"/> |
||||
|
</manifest> |
||||
@ -0,0 +1,44 @@ |
|||||
|
allprojects { |
||||
|
repositories { |
||||
|
mavenLocal() |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
maven { url = uri("https://storage.googleapis.com/download.flutter.io") } |
||||
|
// 添加火山引擎Maven仓库 |
||||
|
maven { |
||||
|
url = uri("https://artifact.bytedance.com/repository/Volcengine/") |
||||
|
} |
||||
|
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } |
||||
|
|
||||
|
// 修改为绝对路径,避免相对路径计算错误 |
||||
|
flatDir { |
||||
|
dirs(file("${rootProject.projectDir}/../local_plugins/qq_music/android/libs")) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() |
||||
|
rootProject.layout.buildDirectory.value(newBuildDir) |
||||
|
|
||||
|
subprojects { |
||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) |
||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir) |
||||
|
} |
||||
|
subprojects { |
||||
|
project.evaluationDependsOn(":app") |
||||
|
} |
||||
|
|
||||
|
tasks.register<Delete>("clean") { |
||||
|
delete(rootProject.layout.buildDirectory) |
||||
|
} |
||||
|
|
||||
|
buildscript { |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
} |
||||
|
dependencies { |
||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") |
||||
|
// 其他依赖... |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError |
||||
|
android.useAndroidX=true |
||||
|
android.enableJetifier=true |
||||
|
android.max.page.size=16384 |
||||
@ -0,0 +1,5 @@ |
|||||
|
distributionBase=GRADLE_USER_HOME |
||||
|
distributionPath=wrapper/dists |
||||
|
zipStoreBase=GRADLE_USER_HOME |
||||
|
zipStorePath=wrapper/dists |
||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip |
||||
@ -0,0 +1,60 @@ |
|||||
|
// This file configures the Flutter project |
||||
|
// Used by the Flutter tool to generate GeneratedPluginRegistrant.java |
||||
|
|
||||
|
pluginManagement { |
||||
|
val flutterSdkPath = run { |
||||
|
val properties = java.util.Properties() |
||||
|
val localPropertiesFile = file("local.properties") |
||||
|
if (localPropertiesFile.exists()) { |
||||
|
localPropertiesFile.inputStream().use { properties.load(it) } |
||||
|
} |
||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk") |
||||
|
requireNotNull(flutterSdkPath) { "flutter.sdk not set in local.properties" } |
||||
|
flutterSdkPath |
||||
|
} |
||||
|
|
||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") |
||||
|
|
||||
|
repositories { |
||||
|
mavenLocal() |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
gradlePluginPortal() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
plugins { |
||||
|
id("dev.flutter.flutter-plugin-loader") |
||||
|
id("com.android.application") version "8.9.1" apply false |
||||
|
id("com.android.library") version "8.9.1" apply false |
||||
|
id("org.jetbrains.kotlin.android") version "2.1.10" apply false |
||||
|
id("com.google.protobuf") version "0.9.4" apply false |
||||
|
} |
||||
|
|
||||
|
include(":app") |
||||
|
include(":azure_speech") |
||||
|
include(":chat_storage") |
||||
|
include(":jieli") |
||||
|
include(":ble_service") |
||||
|
include(":classic_bluetooth") |
||||
|
include(":agent_service") |
||||
|
include(":speech") |
||||
|
include(":chat_api") |
||||
|
include(":bytedance_speech") |
||||
|
include(":qq_music") // 添加这行 |
||||
|
include(":jl_opus") |
||||
|
include(":bluetooth_manager") // 恒玄(BES)经典蓝牙连接 + G722/mSBC 音频编解码 |
||||
|
|
||||
|
// 设置项目路径 |
||||
|
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") |
||||
|
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android") |
||||
|
project(":jieli").projectDir = file("../local_plugins/jieli/android") |
||||
|
project(":ble_service").projectDir = file("../local_plugins/ble_service/android") |
||||
|
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") |
||||
|
project(":agent_service").projectDir = file("../local_plugins/agent_service/android") |
||||
|
project(":speech").projectDir = file("../local_plugins/speech/android") |
||||
|
project(":chat_api").projectDir = file("../local_plugins/chat_api/android") |
||||
|
project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android") |
||||
|
project(":qq_music").projectDir = file("../local_plugins/qq_music/android") // 添加这行 |
||||
|
project(":jl_opus").projectDir = file("../local_plugins/jl_opus/android") |
||||
|
project(":bluetooth_manager").projectDir = file("../local_plugins/bluetooth_manager/android") |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<key>API_KEY</key> |
||||
|
<string>AIzaSyBEQPL-2Qh6fERxrxLP9Pzcm95mTmTpBxE</string> |
||||
|
<key>GCM_SENDER_ID</key> |
||||
|
<string>733281995296</string> |
||||
|
<key>PLIST_VERSION</key> |
||||
|
<string>1</string> |
||||
|
<key>BUNDLE_ID</key> |
||||
|
<string>com.saitong.voitrans</string> |
||||
|
<key>PROJECT_ID</key> |
||||
|
<string>voitrans</string> |
||||
|
<key>STORAGE_BUCKET</key> |
||||
|
<string>voitrans.firebasestorage.app</string> |
||||
|
<key>IS_ADS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_ANALYTICS_ENABLED</key> |
||||
|
<false></false> |
||||
|
<key>IS_APPINVITE_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_GCM_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>IS_SIGNIN_ENABLED</key> |
||||
|
<true></true> |
||||
|
<key>GOOGLE_APP_ID</key> |
||||
|
<string>1:733281995296:ios:6a8351691312100bd70a45</string> |
||||
|
</dict> |
||||
|
</plist> |
||||
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 683 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 614 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 653 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 965 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 568 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 654 B |