Compare commits
No commits in common. 'main' and 'newdev_chengguofeng' have entirely different histories.
main
...
newdev_che
@ -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,159 @@ |
|||||
|
import java.util.Properties |
||||
|
import java.io.FileInputStream |
||||
|
repositories { |
||||
|
google() |
||||
|
mavenCentral() |
||||
|
flatDir { |
||||
|
dirs(project(":spotify_sdk").projectDir.resolve("libs")) |
||||
|
dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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") |
||||
|
} |
||||
|
|
||||
|
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="Voitrans" |
||||
|
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,533 @@ |
|||||
|
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 |
||||
|
if (!pluginRegistry.has(io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin::class.java)) { |
||||
|
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: 4.7 KiB |
|
After Width: | Height: | Size: 858 B |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 464 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 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,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,5 @@ |
|||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError |
||||
|
org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home |
||||
|
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(":spotify_sdk") |
||||
|
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(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/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: 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 |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 965 B |
|
After Width: | Height: | Size: 595 B |