Compare commits
No commits in common. 'main' and 'weicu' have entirely different histories.
|
After Width: | Height: | Size: 76 KiB |
@ -0,0 +1,10 @@ |
|||
{ |
|||
"mcpServers": { |
|||
"web-search": { |
|||
"url": "http://mcp.ideapsound.com:8000/sse" |
|||
}, |
|||
"mp3": { |
|||
"url": "https://api.deapsound.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 @@ |
|||
{"projectName":"trae_deep_voice_k3cu"} |
|||
@ -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. |
|||
|
|||
@ -1,2 +1,60 @@ |
|||
# deepvoice_client |
|||
# 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,14 @@ |
|||
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 |
|||
@ -0,0 +1,166 @@ |
|||
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/voitrans.properties") |
|||
val keystoreProperties =Properties().apply { |
|||
load(FileInputStream(keystorePropertiesFile)) // 使用 Kotlin 扩展方法 |
|||
} |
|||
|
|||
android { |
|||
namespace = "com.saitong.voitrans" |
|||
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( |
|||
"redirectSchemeName" to "spotify-sdk", |
|||
"redirectHostName" to "auth", |
|||
"applicationName" to "com.saitong.voitrans.MyApp" // 新增:声明自定义 Application |
|||
) |
|||
) |
|||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
|||
applicationId = "com.saitong.voitrans" |
|||
// 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.saitong.voitrans" |
|||
} |
|||
}, |
|||
"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.saitong.voitrans.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,4 @@ |
|||
storePassword=saitong_voitrans1122 |
|||
keyPassword=saitong_voitrans1122 |
|||
keyAlias=voitrans |
|||
storeFile=sign/voitrans.keystore |
|||
@ -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,181 @@ |
|||
<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.READ_CONTACTS" /> |
|||
<!-- 添加拨打电话权限 --> |
|||
<uses-permission android:name="android.permission.CALL_PHONE" /> |
|||
<!-- 添加查询应用包权限 --> |
|||
<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="Wicue" |
|||
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" /> |
|||
|
|||
<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: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="voitrans"/> |
|||
</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.saitong.voitrans |
|||
|
|||
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.saitong.voitrans.extra.NAVIGATE_TO" |
|||
private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord" |
|||
private val ROUTE_MEETING_RECORD = "/meeting/record" |
|||
private val DEEPLINK_CHANNEL = "com.saitong.voitrans/deeplink" |
|||
private val DELIVERY_RETRY_DELAY_MS = 300L |
|||
private val MAX_DELIVERY_RETRIES = 12 |
|||
private val NATIVE_LOCALIZATION_CHANNEL = "com.saitong.voitrans/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.saitong.voitrans |
|||
|
|||
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> |
|||
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<background android:drawable="@mipmap/ic_launcher_background"/> |
|||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> |
|||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/> |
|||
</adaptive-icon> |
|||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1013 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 14 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,4 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
<color name="ic_launcher_background">#FFFFFF</color> |
|||
</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,58 @@ |
|||
// 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") |
|||
|
|||
// 设置项目路径 |
|||
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") |
|||
@ -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: 206 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
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: 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 |