55 changed files with 2321 additions and 1302 deletions
@ -1,116 +0,0 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|||
import io.flutter.plugin.common.EventChannel |
|||
import io.flutter.plugin.common.MethodCall |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|||
import io.flutter.plugin.common.MethodChannel.Result |
|||
import android.bluetooth.BluetoothDevice |
|||
import android.bluetooth.BluetoothGatt |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import com.jieli.jl_bt_ota.constant.ErrorCode; |
|||
import com.jieli.jl_bt_ota.constant.StateCode; |
|||
import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; |
|||
import com.jieli.jl_bt_ota.interfaces.BtEventCallback; |
|||
import com.jieli.jl_bt_ota.interfaces.IActionCallback; |
|||
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; |
|||
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; |
|||
import com.jieli.jl_bt_ota.model.base.BaseError; |
|||
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; |
|||
import android.util.Log |
|||
import java.util.* |
|||
class CustomUpdateCallback( |
|||
val context: Context, |
|||
val device: BluetoothDevice? |
|||
) : IUpgradeCallback { |
|||
companion object { |
|||
private const val TAG = "JlOtaPlugin" |
|||
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" |
|||
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" |
|||
|
|||
} |
|||
|
|||
override fun onStartOTA() { |
|||
Log.d(TAG, "onStartOTA开始升级") |
|||
// viewModel.otaStateMLD.value = OTAStart(device) |
|||
} |
|||
|
|||
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { |
|||
Log.d(TAG, "onNeedReconnect需要重新链接") |
|||
|
|||
} |
|||
|
|||
override fun onProgress(type: Int, progress: Float) { |
|||
Log.d(TAG, "onProgress正在进展") |
|||
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress) |
|||
} |
|||
|
|||
override fun onStopOTA() { |
|||
Log.d(TAG, "onStartOTA停止升级") |
|||
|
|||
} |
|||
|
|||
override fun onCancelOTA() { |
|||
Log.d(TAG, "onCancelOTA取消升级") |
|||
|
|||
} |
|||
|
|||
override fun onError(error: BaseError?) { |
|||
val errorMessage = error?.message ?: "Unknown error" |
|||
Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") |
|||
|
|||
// 如果需要打印完整错误对象信息(假设BaseError有重写toString()) |
|||
Log.d(TAG, "完整错误对象: ${error ?: "null"}") |
|||
} |
|||
} |
|||
|
|||
class OTAManager(context: Context) : BluetoothOTAManager(context) { |
|||
|
|||
|
|||
// 定义回调接口 |
|||
interface DataCallback { |
|||
fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) |
|||
} |
|||
// 保存回调的实例 |
|||
private var dataCallback: DataCallback? = null |
|||
|
|||
// 设置回调的方法 |
|||
fun setDataCallback(callback: DataCallback) { |
|||
this.dataCallback = callback |
|||
} |
|||
override fun getConnectedDevice(): BluetoothDevice? { |
|||
// 这里可以添加空实现的注释说明 |
|||
Log.i(TAG, "ota数据发送getConnectedDevice") |
|||
return null |
|||
} |
|||
|
|||
override fun getConnectedBluetoothGatt(): BluetoothGatt? { |
|||
// 这里可以添加空实现的注释说明 |
|||
Log.i(TAG, "ota数据发送getConnectedBluetoothGatt") |
|||
return null |
|||
} |
|||
|
|||
override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { |
|||
// 这里可以添加空实现的注释说明 |
|||
Log.i(TAG, "ota数据发送connectBluetoothDevice") |
|||
} |
|||
|
|||
override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { |
|||
// 这里可以添加空实现的注释说明 |
|||
Log.i(TAG, "ota数据发送disconnectBluetoothDevice") |
|||
} |
|||
|
|||
/** 数据发送方法(协议无关) */ |
|||
override fun sendDataToDevice( |
|||
bluetoothDevice: BluetoothDevice?, |
|||
bytes: ByteArray? |
|||
): Boolean { |
|||
Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}") |
|||
// 触发回调,将数据回传给调用者 |
|||
dataCallback?.onDataReceived(bluetoothDevice, bytes) |
|||
// context.onReceiveDeviceData(device, data); |
|||
return true |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,70 @@ |
|||
# ChatAPI Android 实现 |
|||
|
|||
基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。 |
|||
|
|||
## 特性 |
|||
|
|||
- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端 |
|||
- ✅ 与 iOS 版本接口完全一致 |
|||
- ✅ 支持流式和非流式对话 |
|||
- ✅ 支持多模态内容(文本和图像识别) |
|||
- ✅ 工具调用架构预留(MCP 功能暂时留空) |
|||
- ✅ 协程支持和异步处理 |
|||
- ✅ 完整的错误处理和取消机制 |
|||
|
|||
## 依赖 |
|||
|
|||
- openai-kotlin 4.0.1 |
|||
- ktor-client-okhttp 2.3.2 |
|||
- kotlinx-coroutines |
|||
- gson |
|||
|
|||
## 主要文件 |
|||
|
|||
- `ChatApiPlugin.kt` - Flutter 插件桥接层 |
|||
- `ChatApiService.kt` - 核心 ChatAPI 服务实现 |
|||
- `StreamCallback.kt` - 流式回调接口 |
|||
|
|||
## 与 iOS 版本的一致性 |
|||
|
|||
Android 实现完全遵循 iOS 版本的接口定义: |
|||
|
|||
1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)` |
|||
2. **消息创建**: `createUserMessage()`, `createAssistantMessage()` |
|||
3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式) |
|||
4. **函数注册**: `registerFunction()` |
|||
5. **MCP 相关**: 接口保留,功能暂时留空 |
|||
6. **流式事件**: 完全一致的事件类型和回调 |
|||
|
|||
## MCP 功能状态 |
|||
|
|||
MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括: |
|||
|
|||
- `initializeMcpClient()` - 返回 false |
|||
- `isMcpInitialized()` - 返回 false |
|||
- `handleMcpToolCall()` - 返回占位符消息 |
|||
- 工具调用会通知上层但不会实际执行 |
|||
|
|||
## 使用示例 |
|||
|
|||
```kotlin |
|||
// 初始化 |
|||
chatApiService.initialize( |
|||
apiKey = "your-api-key", |
|||
baseUrl = "https://api.openai.com/v1/", |
|||
model = "gpt-3.5-turbo", |
|||
mcpServer = "" |
|||
) |
|||
|
|||
// 流式对话 |
|||
chatApiService.sendMessageStream(listOf( |
|||
mapOf("role" to "user", "content" to "Hello!") |
|||
)) |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE` |
|||
2. 所有网络请求在后台线程执行 |
|||
3. 流式回调在主线程触发 |
|||
4. 支持取消正在进行的请求 |
|||
@ -0,0 +1,50 @@ |
|||
plugins { |
|||
id("com.android.library") |
|||
id("org.jetbrains.kotlin.android") |
|||
kotlin("plugin.serialization") version "1.9.24" |
|||
} |
|||
|
|||
android { |
|||
namespace = "com.yunqiinnovation.chat_api" |
|||
compileSdk = 35 |
|||
|
|||
defaultConfig { |
|||
minSdk = 21 |
|||
targetSdk = 33 |
|||
} |
|||
|
|||
compileOptions { |
|||
sourceCompatibility = JavaVersion.VERSION_11 |
|||
targetCompatibility = JavaVersion.VERSION_11 |
|||
} |
|||
|
|||
kotlinOptions { |
|||
jvmTarget = "11" |
|||
} |
|||
|
|||
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") |
|||
} |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") |
|||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") |
|||
|
|||
// OpenAI Kotlin 4.0.1 |
|||
implementation("com.aallam.openai:openai-client:4.0.1") |
|||
implementation("io.ktor:ktor-client-okhttp:2.3.2") |
|||
|
|||
// JSON处理 |
|||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") |
|||
implementation("com.google.code.gson:gson:2.10.1") |
|||
} |
|||
@ -0,0 +1 @@ |
|||
rootProject.name = "chat_api" |
|||
@ -0,0 +1,8 @@ |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|||
package="com.yunqiinnovation.chat_api"> |
|||
|
|||
<!-- 网络权限 --> |
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |
|||
|
|||
</manifest> |
|||
@ -0,0 +1,252 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|||
import io.flutter.plugin.common.EventChannel |
|||
import io.flutter.plugin.common.MethodCall |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|||
import io.flutter.plugin.common.MethodChannel.Result |
|||
import kotlinx.coroutines.* |
|||
|
|||
/** |
|||
* ChatApiPlugin |
|||
* |
|||
* 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现 |
|||
* 与 iOS 版本接口完全一致 |
|||
*/ |
|||
class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { |
|||
private lateinit var methodChannel: MethodChannel |
|||
private lateinit var eventChannel: EventChannel |
|||
private var eventSink: EventChannel.EventSink? = null |
|||
|
|||
private val chatApiService = ChatApiService() |
|||
private val mainHandler = Handler(Looper.getMainLooper()) |
|||
|
|||
// 协程作用域 |
|||
private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) |
|||
|
|||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { |
|||
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods") |
|||
methodChannel.setMethodCallHandler(this) |
|||
|
|||
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events") |
|||
eventChannel.setStreamHandler(this) |
|||
|
|||
// 设置流式回调 |
|||
chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback { |
|||
override fun onToken(token: String) { |
|||
sendEvent("token", token) |
|||
} |
|||
|
|||
override fun onComplete() { |
|||
sendEvent("complete", null) |
|||
} |
|||
|
|||
override fun onError(error: Exception) { |
|||
sendEvent("error", error.message ?: "未知错误") |
|||
} |
|||
|
|||
override fun onFunctionCall(functionCall: org.json.JSONObject) { |
|||
try { |
|||
val jsonString = functionCall.toString() |
|||
sendEvent("functionCall", jsonString) |
|||
} catch (e: Exception) { |
|||
sendEvent("error", "Failed to serialize function call: ${e.message}") |
|||
} |
|||
} |
|||
|
|||
override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { |
|||
try { |
|||
val meta = org.json.JSONObject().apply { |
|||
put("functionCall", functionCall) |
|||
put("functionCallResult", functionCallResult) |
|||
} |
|||
val metaString = meta.toString() |
|||
val context = functionCallResult.optString("context", "") |
|||
|
|||
sendEvent("functionCall", context, metaString) |
|||
} catch (e: Exception) { |
|||
sendEvent("error", "Failed to serialize function call result: ${e.message}") |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
|||
methodChannel.setMethodCallHandler(null) |
|||
eventChannel.setStreamHandler(null) |
|||
pluginScope.cancel() |
|||
} |
|||
|
|||
override fun onMethodCall(call: MethodCall, result: Result) { |
|||
when (call.method) { |
|||
"initialize" -> handleInitialize(call, result) |
|||
"createUserMessage" -> handleCreateUserMessage(call, result) |
|||
"createAssistantMessage" -> handleCreateAssistantMessage(call, result) |
|||
"sendMessage" -> handleSendMessage(call, result) |
|||
"sendMessageStream" -> handleSendMessageStream(call, result) |
|||
"sendFunctionCallResult" -> handleSendFunctionCallResult(call, result) |
|||
"cancelCurrentStream" -> handleCancelCurrentStream(result) |
|||
"registerFunction" -> handleRegisterFunction(call, result) |
|||
"initializeMcpClient" -> handleInitializeMcpClient(call, result) |
|||
"isMcpInitialized" -> handleIsMcpInitialized(result) |
|||
"closeMcpClient" -> handleCloseMcpClient(result) |
|||
"handleMcpToolCall" -> handleMcpToolCall(call, result) |
|||
else -> result.notImplemented() |
|||
} |
|||
} |
|||
|
|||
// MARK: - Method Handlers |
|||
|
|||
private fun handleInitialize(call: MethodCall, result: Result) { |
|||
val apiKey = call.argument<String>("apiKey") |
|||
if (apiKey.isNullOrEmpty()) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
val baseUrl = call.argument<String>("baseUrl") ?: "" |
|||
val model = call.argument<String>("model") ?: "" |
|||
val mcpServer = call.argument<String>("mcpServer") ?: "" |
|||
|
|||
val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer) |
|||
result.success(success) |
|||
} |
|||
|
|||
private fun handleCreateUserMessage(call: MethodCall, result: Result) { |
|||
val content = call.argument<String>("content") |
|||
if (content.isNullOrEmpty()) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
val message = chatApiService.createUserMessage(content) |
|||
result.success(message) |
|||
} |
|||
|
|||
private fun handleCreateAssistantMessage(call: MethodCall, result: Result) { |
|||
val content = call.argument<String>("content") |
|||
if (content.isNullOrEmpty()) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
val message = chatApiService.createAssistantMessage(content) |
|||
result.success(message) |
|||
} |
|||
|
|||
private fun handleSendMessage(call: MethodCall, result: Result) { |
|||
@Suppress("UNCHECKED_CAST") |
|||
val messages = call.argument<List<Map<String, Any>>>("messages") |
|||
if (messages == null) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
pluginScope.launch { |
|||
try { |
|||
val response = chatApiService.sendMessage(messages) |
|||
mainHandler.post { |
|||
result.success(response) |
|||
} |
|||
} catch (e: Exception) { |
|||
mainHandler.post { |
|||
result.error("SEND_ERROR", e.message, null) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun handleSendMessageStream(call: MethodCall, result: Result) { |
|||
@Suppress("UNCHECKED_CAST") |
|||
val messages = call.argument<List<Map<String, Any>>>("messages") |
|||
if (messages == null) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
chatApiService.sendMessageStream(messages) |
|||
result.success(true) |
|||
} |
|||
|
|||
private fun handleSendFunctionCallResult(call: MethodCall, result: Result) { |
|||
result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null) |
|||
} |
|||
|
|||
private fun handleCancelCurrentStream(result: Result) { |
|||
val success = chatApiService.cancelCurrentStream() |
|||
result.success(success) |
|||
} |
|||
|
|||
private fun handleRegisterFunction(call: MethodCall, result: Result) { |
|||
val name = call.argument<String>("name") |
|||
val description = call.argument<String>("description") |
|||
@Suppress("UNCHECKED_CAST") |
|||
val parameters = call.argument<Map<String, Any>>("parameters") |
|||
|
|||
if (name.isNullOrEmpty() || description.isNullOrEmpty() || parameters == null) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
val success = chatApiService.registerFunction(name, description, parameters) |
|||
result.success(success) |
|||
} |
|||
|
|||
private fun handleInitializeMcpClient(call: MethodCall, result: Result) { |
|||
val serverUrl = call.argument<String>("serverUrl") |
|||
if (serverUrl.isNullOrEmpty()) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
// MCP功能暂时留空 |
|||
result.success(false) |
|||
} |
|||
|
|||
private fun handleIsMcpInitialized(result: Result) { |
|||
// MCP功能暂时留空 |
|||
result.success(false) |
|||
} |
|||
|
|||
private fun handleCloseMcpClient(result: Result) { |
|||
// MCP功能暂时留空 |
|||
result.success(true) |
|||
} |
|||
|
|||
private fun handleMcpToolCall(call: MethodCall, result: Result) { |
|||
val functionCallJson = call.argument<String>("functionCall") |
|||
if (functionCallJson.isNullOrEmpty()) { |
|||
result.error("INVALID_ARGS", "缺少必要参数", null) |
|||
return |
|||
} |
|||
|
|||
// MCP功能暂时留空 |
|||
result.success("MCP功能暂未实现") |
|||
} |
|||
|
|||
// MARK: - Event Stream Handler |
|||
|
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
eventSink = events |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
eventSink = null |
|||
} |
|||
|
|||
private fun sendEvent(type: String, content: Any?, meta: String? = null) { |
|||
mainHandler.post { |
|||
eventSink?.let { sink -> |
|||
val event = mutableMapOf<String, Any>( |
|||
"type" to type |
|||
) |
|||
content?.let { event["content"] = it } |
|||
meta?.let { event["meta"] = it } |
|||
sink.success(event) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,799 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.graphics.Bitmap |
|||
import android.graphics.BitmapFactory |
|||
import android.util.Base64 |
|||
import com.aallam.openai.api.chat.* |
|||
import com.aallam.openai.api.core.Role |
|||
import com.aallam.openai.api.model.ModelId |
|||
import com.aallam.openai.api.http.Timeout |
|||
import com.aallam.openai.client.OpenAI |
|||
import com.aallam.openai.client.OpenAIConfig |
|||
import com.aallam.openai.client.OpenAIHost |
|||
import com.google.gson.Gson |
|||
import kotlinx.coroutines.* |
|||
import kotlinx.coroutines.flow.collect |
|||
import java.io.ByteArrayOutputStream |
|||
import java.io.File |
|||
import kotlin.coroutines.CoroutineContext |
|||
import kotlin.math.min |
|||
import kotlin.math.sqrt |
|||
import kotlin.time.Duration.Companion.seconds |
|||
import android.util.Log |
|||
|
|||
/** |
|||
* ChatAPI服务异常 |
|||
*/ |
|||
class ChatApiException(message: String) : Exception("ChatApiException: $message") |
|||
|
|||
/** |
|||
* 流式回调接口 |
|||
* 与 iOS 版本 StreamCallback 协议保持完全一致 |
|||
*/ |
|||
interface StreamCallback { |
|||
/** |
|||
* 接收到新的token |
|||
*/ |
|||
fun onToken(token: String) |
|||
|
|||
/** |
|||
* 流式响应完成 |
|||
*/ |
|||
fun onComplete() |
|||
|
|||
/** |
|||
* 出现错误 |
|||
*/ |
|||
fun onError(error: Exception) |
|||
|
|||
/** |
|||
* 函数调用 - 兼容JSONObject格式 |
|||
*/ |
|||
fun onFunctionCall(functionCall: org.json.JSONObject) |
|||
|
|||
/** |
|||
* 函数调用结果 - 兼容JSONObject格式 |
|||
*/ |
|||
fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) |
|||
} |
|||
|
|||
/** |
|||
* 工具调用信息 |
|||
*/ |
|||
private data class ToolCallInfo( |
|||
var id: String = "", |
|||
var name: String = "", |
|||
var arguments: String = "" |
|||
) { |
|||
fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty() |
|||
} |
|||
|
|||
/** |
|||
* ChatAPI服务的Android实现 |
|||
* 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 |
|||
*/ |
|||
class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { |
|||
|
|||
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() |
|||
|
|||
// MARK: - 属性 |
|||
private var baseUrl = "https://api.openai.com/v1/" |
|||
private var apiKey = "" |
|||
private var model = "gpt-3.5-turbo" |
|||
private var visionModel = "gpt-4-vision-preview" |
|||
private var isInitialized = false |
|||
|
|||
// OpenAI 客户端 |
|||
private var openAI: OpenAI? = null |
|||
|
|||
// 流式请求相关 |
|||
private var currentStreamJob: Job? = null |
|||
private var streamCallback: StreamCallback? = null |
|||
private var currentMessages: List<Map<String, Any>> = emptyList() |
|||
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf() |
|||
private var isCanceled = false |
|||
|
|||
// JSON处理 |
|||
private val gson = Gson() |
|||
|
|||
// MARK: - 公共方法 |
|||
|
|||
/** |
|||
* 设置流式回调 |
|||
*/ |
|||
fun setStreamCallback(callback: StreamCallback) { |
|||
streamCallback = callback |
|||
} |
|||
|
|||
/** |
|||
* 初始化ChatAPI服务 |
|||
*/ |
|||
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { |
|||
this.apiKey = apiKey |
|||
if (baseUrl.isNotEmpty()) { |
|||
this.baseUrl = baseUrl |
|||
} |
|||
if (model.isNotEmpty()) { |
|||
this.model = model |
|||
} |
|||
Log.d("ChatApiService", "原始 baseUrl: $baseUrl") |
|||
|
|||
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在) |
|||
// 因为 openai-kotlin 会自动拼接 /chat/completions |
|||
var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) { |
|||
this.baseUrl.removeSuffix("/chat/completions") |
|||
} else { |
|||
this.baseUrl.removeSuffix("/") |
|||
} |
|||
|
|||
// 确保以 / 结尾 |
|||
if (!processedBaseUrl.endsWith("/")) { |
|||
processedBaseUrl += "/" |
|||
} |
|||
|
|||
Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl") |
|||
|
|||
return try { |
|||
// 创建OpenAI配置 |
|||
val config = OpenAIConfig( |
|||
token = apiKey, |
|||
timeout = Timeout(socket = 60.seconds), |
|||
host = OpenAIHost(baseUrl = processedBaseUrl) |
|||
) |
|||
|
|||
openAI = OpenAI(config) |
|||
|
|||
// 初始化MCP客户端 (暂时留空,但保持接口一致) |
|||
if (mcpServer.isNotEmpty()) { |
|||
// MCP功能暂时留空,但记录服务器地址以便后续实现 |
|||
// initializeMcpClient(mcpServer) |
|||
} |
|||
|
|||
isInitialized = apiKey.isNotEmpty() |
|||
true |
|||
} catch (e: Exception) { |
|||
false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建用户消息 |
|||
*/ |
|||
fun createUserMessage(content: String): Map<String, Any> { |
|||
return mapOf( |
|||
"role" to "user", |
|||
"content" to content |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 创建助手消息 |
|||
*/ |
|||
fun createAssistantMessage(content: String): Map<String, Any> { |
|||
return mapOf( |
|||
"role" to "assistant", |
|||
"content" to content |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 创建系统消息 |
|||
*/ |
|||
fun createSystemMessage(content: String): Map<String, Any> { |
|||
return mapOf( |
|||
"role" to "system", |
|||
"content" to content |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 发送消息(非流式输出) |
|||
*/ |
|||
suspend fun sendMessage(messages: List<Map<String, Any>>): String { |
|||
if (!isInitialized || apiKey.isEmpty() || openAI == null) { |
|||
throw ChatApiException("ChatAPI服务未初始化") |
|||
} |
|||
|
|||
// 检查是否包含图片,决定使用哪个模型 |
|||
val currentModel = if (containsImage(messages)) visionModel else model |
|||
|
|||
// 转换消息格式 |
|||
val chatMessages = convertToChatMessages(messages) |
|||
|
|||
// 构建请求 |
|||
val chatCompletionRequest = ChatCompletionRequest( |
|||
model = ModelId(currentModel), |
|||
messages = chatMessages, |
|||
maxTokens = 2000, |
|||
temperature = 0.7 |
|||
) |
|||
|
|||
val result = openAI!!.chatCompletion(chatCompletionRequest) |
|||
|
|||
// 检查是否有工具调用 |
|||
val choice = result.choices.firstOrNull() |
|||
choice?.message?.toolCalls?.firstOrNull()?.let { toolCall -> |
|||
if (toolCall is ToolCall.Function) { |
|||
val functionCallJson = mapOf( |
|||
"name" to toolCall.function.name, |
|||
"arguments" to (toolCall.function.arguments ?: ""), |
|||
"id" to toolCall.id.toString() |
|||
) |
|||
return gson.toJson(functionCallJson) |
|||
} |
|||
} |
|||
|
|||
// 返回普通消息内容 |
|||
return choice?.message?.content ?: throw ChatApiException("无效的响应格式") |
|||
} |
|||
|
|||
/** |
|||
* 发送消息(流式输出)- 兼容JSONArray格式 |
|||
*/ |
|||
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { |
|||
// 设置回调 |
|||
setStreamCallback(callback) |
|||
|
|||
// 转换JSONArray为List<Map<String, Any>> |
|||
val messageList = convertJsonArrayToList(messages) |
|||
|
|||
// 调用原始方法 |
|||
sendMessageStream(messageList) |
|||
} |
|||
|
|||
/** |
|||
* 发送消息(流式输出) |
|||
*/ |
|||
fun sendMessageStream(messages: List<Map<String, Any>>) { |
|||
if (!isInitialized || apiKey.isEmpty() || openAI == null) { |
|||
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化")) |
|||
return |
|||
} |
|||
|
|||
// 重置状态 |
|||
isCanceled = false |
|||
currentMessages = messages |
|||
toolCalls.clear() |
|||
|
|||
// 检查是否包含图片,决定使用哪个模型 |
|||
val currentModel = if (containsImage(messages)) visionModel else model |
|||
|
|||
currentStreamJob = launch { |
|||
try { |
|||
// 转换消息格式 |
|||
val chatMessages = convertToChatMessages(messages) |
|||
|
|||
// 构建请求 |
|||
val chatCompletionRequest = ChatCompletionRequest( |
|||
model = ModelId(currentModel), |
|||
messages = chatMessages, |
|||
maxTokens = 2000, |
|||
temperature = 0.7 |
|||
) |
|||
|
|||
val chatsFlow = openAI!!.chatCompletions(chatCompletionRequest) |
|||
|
|||
chatsFlow.collect { result -> |
|||
if (isCanceled) return@collect |
|||
|
|||
val choice = result.choices.firstOrNull() ?: return@collect |
|||
val delta = choice.delta ?: return@collect |
|||
|
|||
// 处理普通文本内容 |
|||
delta.content?.let { content -> |
|||
streamCallback?.onToken(content) |
|||
} |
|||
|
|||
// 收集工具调用信息 |
|||
delta.toolCalls?.forEach { toolCall -> |
|||
val index = toolCall.index |
|||
|
|||
// 创建或获取现有的工具调用信息 |
|||
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } |
|||
|
|||
// 更新ID |
|||
toolCall.id?.let { toolCallInfo.id = it.toString() } |
|||
|
|||
// 更新函数信息 |
|||
toolCall.function?.let { function -> |
|||
function.name?.let { toolCallInfo.name = it } |
|||
function.arguments?.let { toolCallInfo.arguments += it } |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (!isCanceled) { |
|||
// 处理工具调用或完成 |
|||
val hasToolCalls = processToolCalls() |
|||
if (!hasToolCalls) { |
|||
streamCallback?.onComplete() |
|||
} |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
if (!isCanceled) { |
|||
streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理工具调用 |
|||
*/ |
|||
private suspend fun processToolCalls(): Boolean { |
|||
val firstToolCall = toolCalls.values.firstOrNull { it.isValid() } ?: return false |
|||
|
|||
// 创建函数调用字典 |
|||
val functionCall = mapOf( |
|||
"name" to firstToolCall.name, |
|||
"arguments" to firstToolCall.arguments, |
|||
"id" to firstToolCall.id |
|||
) |
|||
|
|||
// 通知上层工具调用事件 |
|||
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) |
|||
|
|||
// 在后台队列处理工具调用 |
|||
launch { |
|||
try { |
|||
if (!isCanceled) { |
|||
// 这里暂时返回占位符结果,实际MCP功能留空 |
|||
val result = mapOf("context" to "MCP功能暂未实现") |
|||
|
|||
if (!isCanceled) { |
|||
// 处理结果 |
|||
streamCallback?.onFunctionCallResult( |
|||
convertMapToJsonObject(functionCall), |
|||
convertMapToJsonObject(result) |
|||
) |
|||
|
|||
// 将结果发送回OpenAI继续对话 |
|||
sendFunctionCallResultInternal( |
|||
messages = currentMessages, |
|||
functionCall = functionCall, |
|||
functionResult = gson.toJson(result) |
|||
) |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
if (!isCanceled) { |
|||
val errorMessage = "工具调用处理失败: ${e.message}" |
|||
sendFunctionCallResultInternal( |
|||
messages = currentMessages, |
|||
functionCall = functionCall, |
|||
functionResult = errorMessage |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 发送函数调用结果(内部方法) |
|||
*/ |
|||
private suspend fun sendFunctionCallResultInternal( |
|||
messages: List<Map<String, Any>>, |
|||
functionCall: Map<String, Any>, |
|||
functionResult: String |
|||
) { |
|||
if (isCanceled) return |
|||
|
|||
val fullMessages = messages.toMutableList() |
|||
|
|||
// 添加函数调用消息 |
|||
val callId = functionCall["id"] as? String ?: "call_${System.currentTimeMillis()}" |
|||
fullMessages.add( |
|||
mapOf( |
|||
"role" to "assistant", |
|||
"content" to "", |
|||
"tool_calls" to listOf( |
|||
mapOf( |
|||
"id" to callId, |
|||
"type" to "function", |
|||
"function" to mapOf( |
|||
"name" to (functionCall["name"] as? String ?: ""), |
|||
"arguments" to (functionCall["arguments"] as? String ?: "{}") |
|||
) |
|||
) |
|||
) |
|||
) |
|||
) |
|||
|
|||
// 添加函数结果消息 |
|||
fullMessages.add( |
|||
mapOf( |
|||
"role" to "tool", |
|||
"content" to functionResult, |
|||
"tool_call_id" to callId |
|||
) |
|||
) |
|||
|
|||
// 重新发起对话,让AI基于工具调用结果生成回复 |
|||
sendMessageStream(fullMessages) |
|||
} |
|||
|
|||
/** |
|||
* 取消当前流式请求 |
|||
*/ |
|||
fun cancelCurrentStream(): Boolean { |
|||
isCanceled = true |
|||
currentStreamJob?.cancel() |
|||
currentStreamJob = null |
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 取消所有操作并释放资源 (与 iOS 版本保持一致) |
|||
*/ |
|||
fun cancelAll() { |
|||
cancelCurrentStream() |
|||
} |
|||
|
|||
/** |
|||
* 注册函数 |
|||
*/ |
|||
fun registerFunction(name: String, description: String, parameters: Map<String, Any>): Boolean { |
|||
// 暂时返回true,实际功能留空,但保持与iOS版本接口一致 |
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 初始化MCP客户端 |
|||
*/ |
|||
fun initializeMcpClient(serverUrl: String): Boolean { |
|||
// MCP功能暂时留空,但保持与iOS版本接口一致 |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* MCP客户端是否已初始化 |
|||
*/ |
|||
fun isMcpInitialized(): Boolean { |
|||
// MCP功能暂时留空,但保持与iOS版本接口一致 |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 关闭MCP客户端 |
|||
*/ |
|||
fun closeMcpClient() { |
|||
// MCP功能暂时留空,但保持与iOS版本接口一致 |
|||
} |
|||
|
|||
/** |
|||
* 处理MCP工具调用 |
|||
*/ |
|||
suspend fun handleMcpToolCall(functionCallJson: String): String { |
|||
// MCP功能暂时留空,但保持与iOS版本接口一致 |
|||
return "MCP功能暂未实现" |
|||
} |
|||
|
|||
// MARK: - 工具方法 |
|||
|
|||
/** |
|||
* 检查消息中是否包含图片 |
|||
*/ |
|||
private fun containsImage(messages: List<Map<String, Any>>): Boolean { |
|||
for (message in messages) { |
|||
val content = message["content"] |
|||
if (content is String && content.contains("image_url")) { |
|||
return true |
|||
} |
|||
if (content is List<*>) { |
|||
content.forEach { item -> |
|||
if (item is Map<*, *>) { |
|||
val type = item["type"] as? String |
|||
if (type == "image_url") { |
|||
return true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 转换为OpenAI ChatMessage格式 |
|||
*/ |
|||
private fun convertToChatMessages(messages: List<Map<String, Any>>): List<ChatMessage> { |
|||
val chatMessages = mutableListOf<ChatMessage>() |
|||
|
|||
for (message in messages) { |
|||
val role = message["role"] as? String ?: continue |
|||
|
|||
val chatRole = when (role) { |
|||
"system" -> Role.System |
|||
"user" -> Role.User |
|||
"assistant" -> Role.Assistant |
|||
"tool" -> Role.Tool |
|||
else -> Role.User |
|||
} |
|||
|
|||
// 特殊处理assistant角色的tool_calls消息 |
|||
if (role == "assistant" && message.containsKey("tool_calls")) { |
|||
val content = message["content"] as? String ?: "" |
|||
|
|||
// 解析tool_calls |
|||
val toolCallsList = mutableListOf<ToolCall>() |
|||
@Suppress("UNCHECKED_CAST") |
|||
val toolCallsArray = message["tool_calls"] as? List<Map<String, Any>> |
|||
|
|||
toolCallsArray?.forEach { toolCallDict -> |
|||
val id = toolCallDict["id"] as? String ?: return@forEach |
|||
val type = toolCallDict["type"] as? String |
|||
if (type == "function") { |
|||
@Suppress("UNCHECKED_CAST") |
|||
val function = toolCallDict["function"] as? Map<String, Any> ?: return@forEach |
|||
val name = function["name"] as? String ?: return@forEach |
|||
val arguments = function["arguments"] as? String ?: "" |
|||
|
|||
val functionCall = FunctionCall( |
|||
nameOrNull = name, |
|||
argumentsOrNull = arguments |
|||
) |
|||
|
|||
val toolCall = ToolCall.Function( |
|||
id = ToolId(id), |
|||
function = functionCall |
|||
) |
|||
|
|||
toolCallsList.add(toolCall) |
|||
} |
|||
} |
|||
|
|||
val chatMessage = ChatMessage.Assistant( |
|||
content = if (content.isEmpty()) null else content, |
|||
toolCalls = if (toolCallsList.isEmpty()) null else toolCallsList |
|||
) |
|||
|
|||
chatMessages.add(chatMessage) |
|||
continue |
|||
} |
|||
|
|||
// 特殊处理tool角色消息 |
|||
if (role == "tool") { |
|||
val content = message["content"] as? String ?: "" |
|||
val toolCallId = message["tool_call_id"] as? String ?: "" |
|||
|
|||
val chatMessage = ChatMessage.Tool( |
|||
content = content, |
|||
toolCallId = ToolId(toolCallId) |
|||
) |
|||
|
|||
chatMessages.add(chatMessage) |
|||
continue |
|||
} |
|||
|
|||
// 处理普通消息的不同类型内容 |
|||
val content = message["content"] |
|||
when (content) { |
|||
is String -> { |
|||
val chatMessage = when (chatRole) { |
|||
Role.System -> ChatMessage.System(content) |
|||
Role.User -> ChatMessage.User(content) |
|||
Role.Assistant -> ChatMessage.Assistant(content) |
|||
else -> ChatMessage.User(content) |
|||
} |
|||
chatMessages.add(chatMessage) |
|||
} |
|||
is List<*> -> { |
|||
// 对于多模态内容,提取文本部分(简化处理) |
|||
var textContent = "" |
|||
|
|||
content.forEach { item -> |
|||
if (item is Map<*, *>) { |
|||
val type = item["type"] as? String |
|||
if (type == "text") { |
|||
val text = item["text"] as? String ?: "" |
|||
textContent += text |
|||
} |
|||
} |
|||
} |
|||
|
|||
val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent |
|||
|
|||
val chatMessage = when (chatRole) { |
|||
Role.System -> ChatMessage.System(finalContent) |
|||
Role.User -> ChatMessage.User(finalContent) |
|||
Role.Assistant -> ChatMessage.Assistant(finalContent) |
|||
else -> ChatMessage.User(finalContent) |
|||
} |
|||
chatMessages.add(chatMessage) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return chatMessages |
|||
} |
|||
|
|||
/** |
|||
* 将文件转换为Base64字符串 |
|||
*/ |
|||
fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? { |
|||
return try { |
|||
val file = File(filePath) |
|||
if (!file.exists() || !file.isFile) { |
|||
return null |
|||
} |
|||
|
|||
// 读取文件 |
|||
var bitmap = BitmapFactory.decodeFile(filePath) ?: return null |
|||
|
|||
// 检查图片尺寸,限制最大为1024*1024 |
|||
val maxDimension = 1024 |
|||
if (bitmap.width > maxDimension || bitmap.height > maxDimension) { |
|||
// 计算缩放比例,保持纵横比 |
|||
val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height) |
|||
|
|||
val newWidth = (bitmap.width * ratio).toInt() |
|||
val newHeight = (bitmap.height * ratio).toInt() |
|||
|
|||
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) |
|||
} |
|||
|
|||
// 压缩图片 |
|||
val outputStream = ByteArrayOutputStream() |
|||
var quality = 100 |
|||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) |
|||
|
|||
// 如果原始图片太大,继续优化文件大小 |
|||
var fileSize = file.length() / 1024 // 转为KB |
|||
if (fileSize > maxSizeKB) { |
|||
val scale = sqrt(maxSizeKB.toDouble() / fileSize) |
|||
val newWidth = (bitmap.width * scale).toInt() |
|||
val newHeight = (bitmap.height * scale).toInt() |
|||
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) |
|||
outputStream.reset() |
|||
quality = 85 |
|||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) |
|||
} |
|||
|
|||
// 转为Base64 |
|||
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) |
|||
} catch (e: Exception) { |
|||
null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建带图片的用户消息 |
|||
*/ |
|||
fun createUserMessageWithImage(text: String, imageBase64: String): Map<String, Any> { |
|||
val contentArray = mutableListOf<Map<String, Any>>() |
|||
|
|||
// 添加文本部分 |
|||
if (text.isNotEmpty()) { |
|||
contentArray.add( |
|||
mapOf( |
|||
"type" to "text", |
|||
"text" to text |
|||
) |
|||
) |
|||
} |
|||
|
|||
// 添加图片部分 |
|||
contentArray.add( |
|||
mapOf( |
|||
"type" to "image_url", |
|||
"image_url" to mapOf( |
|||
"url" to "data:image/jpeg;base64,$imageBase64" |
|||
) |
|||
) |
|||
) |
|||
|
|||
return mapOf( |
|||
"role" to "user", |
|||
"content" to contentArray |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 从URL中提取主机信息 (保留向后兼容) |
|||
*/ |
|||
private fun extractHost(url: String): String { |
|||
return try { |
|||
val javaUrl = java.net.URL(url) |
|||
javaUrl.host ?: "api.openai.com" |
|||
} catch (e: Exception) { |
|||
"api.openai.com" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 解析JSON参数 (与 iOS 版本保持一致) |
|||
*/ |
|||
private fun parseJsonArguments(argumentsJson: String): Map<String, Any> { |
|||
return try { |
|||
if (argumentsJson.isBlank()) { |
|||
emptyMap() |
|||
} else { |
|||
val jsonObject = org.json.JSONObject(argumentsJson) |
|||
convertJsonObjectToMap(jsonObject) |
|||
} |
|||
} catch (e: Exception) { |
|||
emptyMap() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 转换JSONArray为List<Map<String, Any>> |
|||
*/ |
|||
private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List<Map<String, Any>> { |
|||
val result = mutableListOf<Map<String, Any>>() |
|||
|
|||
for (i in 0 until jsonArray.length()) { |
|||
val jsonObject = jsonArray.optJSONObject(i) ?: continue |
|||
val map = convertJsonObjectToMap(jsonObject) |
|||
result.add(map) |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
/** |
|||
* 转换JSONObject为Map<String, Any> |
|||
*/ |
|||
private fun convertJsonObjectToMap(jsonObject: org.json.JSONObject): Map<String, Any> { |
|||
val map = mutableMapOf<String, Any>() |
|||
|
|||
val keys = jsonObject.keys() |
|||
while (keys.hasNext()) { |
|||
val key = keys.next() |
|||
val value = jsonObject.get(key) |
|||
|
|||
when (value) { |
|||
is org.json.JSONObject -> map[key] = convertJsonObjectToMap(value) |
|||
is org.json.JSONArray -> { |
|||
val list = mutableListOf<Any>() |
|||
for (i in 0 until value.length()) { |
|||
val item = value.get(i) |
|||
when (item) { |
|||
is org.json.JSONObject -> list.add(convertJsonObjectToMap(item)) |
|||
else -> list.add(item) |
|||
} |
|||
} |
|||
map[key] = list |
|||
} |
|||
else -> map[key] = value |
|||
} |
|||
} |
|||
|
|||
return map |
|||
} |
|||
|
|||
/** |
|||
* 转换Map为JSONObject |
|||
*/ |
|||
private fun convertMapToJsonObject(map: Map<String, Any>): org.json.JSONObject { |
|||
val jsonObject = org.json.JSONObject() |
|||
|
|||
for ((key, value) in map) { |
|||
when (value) { |
|||
is Map<*, *> -> { |
|||
@Suppress("UNCHECKED_CAST") |
|||
jsonObject.put(key, convertMapToJsonObject(value as Map<String, Any>)) |
|||
} |
|||
is List<*> -> { |
|||
val jsonArray = org.json.JSONArray() |
|||
value.forEach { item -> |
|||
when (item) { |
|||
is Map<*, *> -> { |
|||
@Suppress("UNCHECKED_CAST") |
|||
jsonArray.put(convertMapToJsonObject(item as Map<String, Any>)) |
|||
} |
|||
else -> jsonArray.put(item) |
|||
} |
|||
} |
|||
jsonObject.put(key, jsonArray) |
|||
} |
|||
else -> jsonObject.put(key, value) |
|||
} |
|||
} |
|||
|
|||
return jsonObject |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
# Miscellaneous |
|||
*.class |
|||
*.log |
|||
*.pyc |
|||
*.swp |
|||
.DS_Store |
|||
.atom/ |
|||
.build/ |
|||
.buildlog/ |
|||
.history |
|||
.svn/ |
|||
.swiftpm/ |
|||
migrate_working_dir/ |
|||
|
|||
# IntelliJ related |
|||
*.iml |
|||
*.ipr |
|||
*.iws |
|||
.idea/ |
|||
|
|||
# The .vscode folder contains launch configuration and tasks you configure in |
|||
# VS Code which you may wish to be included in version control, so this line |
|||
# is commented out by default. |
|||
#.vscode/ |
|||
|
|||
# Flutter/Dart/Pub related |
|||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. |
|||
/pubspec.lock |
|||
**/doc/api/ |
|||
.dart_tool/ |
|||
.flutter-plugins |
|||
.flutter-plugins-dependencies |
|||
build/ |
|||
@ -1,33 +0,0 @@ |
|||
# 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: "ea121f8859e4b13e47a8f845e4586164519588bc" |
|||
channel: "stable" |
|||
|
|||
project_type: plugin |
|||
|
|||
# Tracks metadata for the flutter migrate command |
|||
migration: |
|||
platforms: |
|||
- platform: root |
|||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
- platform: android |
|||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
- platform: ios |
|||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc |
|||
|
|||
# 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' |
|||
@ -1,3 +0,0 @@ |
|||
## 0.0.1 |
|||
|
|||
* TODO: Describe initial release. |
|||
@ -1 +0,0 @@ |
|||
TODO: Add your license here. |
|||
@ -1,15 +0,0 @@ |
|||
# jl_ota |
|||
|
|||
A new Flutter project. |
|||
|
|||
## Getting Started |
|||
|
|||
This project is a starting point for a Flutter |
|||
[plug-in package](https://flutter.dev/to/develop-plugins), |
|||
a specialized package that includes platform-specific implementation code for |
|||
Android and/or iOS. |
|||
|
|||
For help getting started with Flutter development, view the |
|||
[online documentation](https://docs.flutter.dev), which offers tutorials, |
|||
samples, guidance on mobile development, and a full API reference. |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
include: package:flutter_lints/flutter.yaml |
|||
|
|||
# Additional information about this file can be found at |
|||
# https://dart.dev/guides/language/analysis-options |
|||
@ -1,9 +0,0 @@ |
|||
*.iml |
|||
.gradle |
|||
/local.properties |
|||
/.idea/workspace.xml |
|||
/.idea/libraries |
|||
.DS_Store |
|||
/build |
|||
/captures |
|||
.cxx |
|||
@ -1,68 +0,0 @@ |
|||
group = "com.yunqiinnovation.jl_ota" |
|||
version = "1.0-SNAPSHOT" |
|||
|
|||
buildscript { |
|||
ext.kotlin_version = "1.8.22" |
|||
repositories { |
|||
google() |
|||
mavenCentral() |
|||
} |
|||
|
|||
dependencies { |
|||
classpath("com.android.tools.build:gradle:8.7.0") |
|||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") |
|||
} |
|||
} |
|||
|
|||
allprojects { |
|||
repositories { |
|||
google() |
|||
mavenCentral() |
|||
} |
|||
} |
|||
|
|||
apply plugin: "com.android.library" |
|||
apply plugin: "kotlin-android" |
|||
|
|||
android { |
|||
namespace = "com.yunqiinnovation.jl_ota" |
|||
|
|||
compileSdk = 35 |
|||
|
|||
compileOptions { |
|||
sourceCompatibility = JavaVersion.VERSION_11 |
|||
targetCompatibility = JavaVersion.VERSION_11 |
|||
} |
|||
|
|||
kotlinOptions { |
|||
jvmTarget = JavaVersion.VERSION_11 |
|||
} |
|||
|
|||
sourceSets { |
|||
main.java.srcDirs += "src/main/kotlin" |
|||
test.java.srcDirs += "src/test/kotlin" |
|||
} |
|||
|
|||
defaultConfig { |
|||
minSdk = 21 |
|||
} |
|||
|
|||
dependencies { |
|||
// implementation project(':ble_service') |
|||
|
|||
testImplementation("org.jetbrains.kotlin:kotlin-test") |
|||
testImplementation("org.mockito:mockito-core:5.0.0") |
|||
} |
|||
|
|||
testOptions { |
|||
unitTests.all { |
|||
useJUnitPlatform() |
|||
|
|||
testLogging { |
|||
events "passed", "skipped", "failed", "standardOut", "standardError" |
|||
outputs.upToDateWhen {false} |
|||
showStandardStreams = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -1 +0,0 @@ |
|||
rootProject.name = 'jl_ota' |
|||
@ -1,3 +0,0 @@ |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|||
package="com.yunqiinnovation.jl_ota"> |
|||
</manifest> |
|||
@ -1,76 +0,0 @@ |
|||
package com.yunqiinnovation.jl_ota |
|||
|
|||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|||
import io.flutter.plugin.common.EventChannel |
|||
import io.flutter.plugin.common.MethodCall |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|||
import io.flutter.plugin.common.MethodChannel.Result |
|||
|
|||
import android.bluetooth.BluetoothGatt; |
|||
import android.bluetooth.BluetoothDevice |
|||
import android.content.Context |
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
import android.util.Log |
|||
import androidx.annotation.NonNull |
|||
//import com.yunqiinnovation.ble_service.BleService |
|||
|
|||
/** JlOtaPlugin */ |
|||
class JlOtaPlugin() : FlutterPlugin, MethodCallHandler { |
|||
/// The MethodChannel that will the communication between Flutter and native Android |
|||
/// |
|||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it |
|||
/// when the Flutter Engine is detached from the Activity |
|||
private lateinit var channel: MethodChannel |
|||
private lateinit var context: Context |
|||
private lateinit var eventChannel: EventChannel |
|||
private var streamSink: EventChannel.EventSink? = null |
|||
|
|||
companion object { |
|||
private const val TAG = "JlOtaPlugin" |
|||
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" |
|||
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" |
|||
|
|||
} |
|||
|
|||
|
|||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { |
|||
context = flutterPluginBinding.applicationContext |
|||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") |
|||
channel.setMethodCallHandler(this) |
|||
|
|||
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) |
|||
|
|||
eventChannel.setStreamHandler(object : EventChannel.StreamHandler { |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
streamSink = events |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
streamSink = null |
|||
} |
|||
}) |
|||
|
|||
} |
|||
|
|||
override fun onMethodCall(call: MethodCall, result: Result) { |
|||
|
|||
if (call.method == "getPlatformVersion") { |
|||
|
|||
// BleService.otaManager?.configureOTA() |
|||
|
|||
// BleService.otaManager?.startOTA() |
|||
result.success("Android ${android.os.Build.VERSION.RELEASE}") |
|||
} else { |
|||
result.notImplemented() |
|||
} |
|||
} |
|||
|
|||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { |
|||
channel.setMethodCallHandler(null) |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
@ -1,38 +0,0 @@ |
|||
.idea/ |
|||
.vagrant/ |
|||
.sconsign.dblite |
|||
.svn/ |
|||
|
|||
.DS_Store |
|||
*.swp |
|||
profile |
|||
|
|||
DerivedData/ |
|||
build/ |
|||
GeneratedPluginRegistrant.h |
|||
GeneratedPluginRegistrant.m |
|||
|
|||
.generated/ |
|||
|
|||
*.pbxuser |
|||
*.mode1v3 |
|||
*.mode2v3 |
|||
*.perspectivev3 |
|||
|
|||
!default.pbxuser |
|||
!default.mode1v3 |
|||
!default.mode2v3 |
|||
!default.perspectivev3 |
|||
|
|||
xcuserdata |
|||
|
|||
*.moved-aside |
|||
|
|||
*.pyc |
|||
*sync/ |
|||
Icon? |
|||
.tags* |
|||
|
|||
/Flutter/Generated.xcconfig |
|||
/Flutter/ephemeral/ |
|||
/Flutter/flutter_export_environment.sh |
|||
@ -1,19 +0,0 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
public class JlOtaPlugin: NSObject, FlutterPlugin { |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "jl_ota", binaryMessenger: registrar.messenger()) |
|||
let instance = JlOtaPlugin() |
|||
registrar.addMethodCallDelegate(instance, channel: channel) |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
switch call.method { |
|||
case "getPlatformVersion": |
|||
result("iOS " + UIDevice.current.systemVersion) |
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
<?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>NSPrivacyTrackingDomains</key> |
|||
<array/> |
|||
<key>NSPrivacyAccessedAPITypes</key> |
|||
<array/> |
|||
<key>NSPrivacyCollectedDataTypes</key> |
|||
<array/> |
|||
<key>NSPrivacyTracking</key> |
|||
<false/> |
|||
</dict> |
|||
</plist> |
|||
@ -1,29 +0,0 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# Run `pod lib lint jl_ota.podspec` to validate before publishing. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'jl_ota' |
|||
s.version = '0.0.1' |
|||
s.summary = 'A new Flutter project.' |
|||
s.description = <<-DESC |
|||
A new Flutter project. |
|||
DESC |
|||
s.homepage = 'http://example.com' |
|||
s.license = { :file => '../LICENSE' } |
|||
s.author = { 'Your Company' => 'email@example.com' } |
|||
s.source = { :path => '.' } |
|||
s.source_files = 'Classes/**/*' |
|||
s.dependency 'Flutter' |
|||
s.platform = :ios, '12.0' |
|||
|
|||
# Flutter.framework does not contain a i386 slice. |
|||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
|||
s.swift_version = '5.0' |
|||
|
|||
# If your plugin requires a privacy manifest, for example if it uses any |
|||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your |
|||
# plugin's privacy impact, and then uncomment this line. For more information, |
|||
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files |
|||
# s.resource_bundles = {'jl_ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']} |
|||
end |
|||
@ -1,8 +0,0 @@ |
|||
|
|||
import 'jl_ota_platform_interface.dart'; |
|||
|
|||
class JlOta { |
|||
Future<String?> getPlatformVersion() { |
|||
return JlOtaPlatform.instance.getPlatformVersion(); |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
import 'package:flutter/foundation.dart'; |
|||
import 'package:flutter/services.dart'; |
|||
|
|||
import 'jl_ota_platform_interface.dart'; |
|||
|
|||
/// An implementation of [JlOtaPlatform] that uses method channels. |
|||
class MethodChannelJlOta extends JlOtaPlatform { |
|||
/// The method channel used to interact with the native platform. |
|||
@visibleForTesting |
|||
final methodChannel = const MethodChannel('jl_ota'); |
|||
|
|||
@override |
|||
Future<String?> getPlatformVersion() async { |
|||
final version = await methodChannel.invokeMethod<String>('getPlatformVersion'); |
|||
return version; |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; |
|||
|
|||
import 'jl_ota_method_channel.dart'; |
|||
|
|||
abstract class JlOtaPlatform extends PlatformInterface { |
|||
/// Constructs a JlOtaPlatform. |
|||
JlOtaPlatform() : super(token: _token); |
|||
|
|||
static final Object _token = Object(); |
|||
|
|||
static JlOtaPlatform _instance = MethodChannelJlOta(); |
|||
|
|||
/// The default instance of [JlOtaPlatform] to use. |
|||
/// |
|||
/// Defaults to [MethodChannelJlOta]. |
|||
static JlOtaPlatform get instance => _instance; |
|||
|
|||
/// Platform-specific implementations should set this with their own |
|||
/// platform-specific class that extends [JlOtaPlatform] when |
|||
/// they register themselves. |
|||
static set instance(JlOtaPlatform instance) { |
|||
PlatformInterface.verifyToken(instance, _token); |
|||
_instance = instance; |
|||
} |
|||
|
|||
Future<String?> getPlatformVersion() { |
|||
throw UnimplementedError('platformVersion() has not been implemented.'); |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
name: jl_ota |
|||
description: "A new Flutter project." |
|||
version: 0.0.1 |
|||
homepage: |
|||
|
|||
environment: |
|||
sdk: ^3.7.0 |
|||
flutter: '>=3.3.0' |
|||
|
|||
dependencies: |
|||
flutter: |
|||
sdk: flutter |
|||
plugin_platform_interface: ^2.0.2 |
|||
|
|||
dev_dependencies: |
|||
flutter_test: |
|||
sdk: flutter |
|||
flutter_lints: ^5.0.0 |
|||
|
|||
# For information on the generic Dart part of this file, see the |
|||
# following page: https://dart.dev/tools/pub/pubspec |
|||
|
|||
# The following section is specific to Flutter packages. |
|||
flutter: |
|||
# This section identifies this Flutter project as a plugin project. |
|||
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) |
|||
# which should be registered in the plugin registry. This is required for |
|||
# using method channels. |
|||
# The Android 'package' specifies package in which the registered class is. |
|||
# This is required for using method channels on Android. |
|||
# The 'ffiPlugin' specifies that native code should be built and bundled. |
|||
# This is required for using `dart:ffi`. |
|||
# All these are used by the tooling to maintain consistency when |
|||
# adding or updating assets for this project. |
|||
plugin: |
|||
platforms: |
|||
android: |
|||
package: com.yunqiinnovation.jl_ota |
|||
pluginClass: JlOtaPlugin |
|||
ios: |
|||
pluginClass: JlOtaPlugin |
|||
|
|||
# To add assets to your plugin package, add an assets section, like this: |
|||
# assets: |
|||
# - images/a_dot_burr.jpeg |
|||
# - images/a_dot_ham.jpeg |
|||
# |
|||
# For details regarding assets in packages, see |
|||
# https://flutter.dev/to/asset-from-package |
|||
# |
|||
# An image asset can refer to one or more resolution-specific "variants", see |
|||
# https://flutter.dev/to/resolution-aware-images |
|||
|
|||
# To add custom fonts to your plugin package, add a fonts section here, |
|||
# in this "flutter" section. Each entry in this list should have a |
|||
# "family" key with the font family name, and a "fonts" key with a |
|||
# list giving the asset and other descriptors for the font. For |
|||
# example: |
|||
# fonts: |
|||
# - family: Schyler |
|||
# fonts: |
|||
# - asset: fonts/Schyler-Regular.ttf |
|||
# - asset: fonts/Schyler-Italic.ttf |
|||
# style: italic |
|||
# - family: Trajan Pro |
|||
# fonts: |
|||
# - asset: fonts/TrajanPro.ttf |
|||
# - asset: fonts/TrajanPro_Bold.ttf |
|||
# weight: 700 |
|||
# |
|||
# For details regarding fonts in packages, see |
|||
# https://flutter.dev/to/font-from-package |
|||
Binary file not shown.
@ -1,2 +0,0 @@ |
|||
#Wed May 21 01:03:35 CST 2025 |
|||
gradle.version=8.9 |
|||
@ -1,26 +0,0 @@ |
|||
package com.jieli.otasdk.model |
|||
|
|||
import android.bluetooth.BluetoothDevice |
|||
|
|||
/** |
|||
* @author zqjasonZhong |
|||
* @since 2022/9/9 |
|||
* @email zhongzhuocheng@zh-jieli.com |
|||
* @desc 设备连接状态 |
|||
*/ |
|||
class DeviceConnection(val device: BluetoothDevice?, var state: Int) { |
|||
|
|||
override fun hashCode(): Int { |
|||
return device.hashCode() |
|||
} |
|||
|
|||
override fun equals(other: Any?): Boolean { |
|||
if (other !is DeviceConnection) return false |
|||
return device == other.device |
|||
} |
|||
|
|||
override fun toString(): String { |
|||
return "DeviceConnection(device=$device, state=$state)" |
|||
} |
|||
|
|||
} |
|||
@ -1,19 +1,142 @@ |
|||
import 'ota_platform_interface.dart'; |
|||
import 'dart:async'; |
|||
import 'dart:typed_data'; |
|||
import 'package:flutter/services.dart'; |
|||
|
|||
class OtaEvent { |
|||
static final int NOT_STARTED = -1; // 未开始 |
|||
static final int VALIDATION_FILE = 0; // 验证文件 |
|||
static final int ONUPGRADE = 1; // 升级中 |
|||
static final int START_UPGRADING = 2; // 开始升级 |
|||
static final int UPGRADE_SUCCESS = 3; // 升级成功 |
|||
static final int UPGRADE_FAILED = 4; // 升级失败 |
|||
static final int CONNECTION_DISCONNECTION = 5; // 连接断开 |
|||
static final int CONNECTION_SUCCESS = 6; // 连接成功 |
|||
|
|||
/// 事件类型 |
|||
final String event; |
|||
|
|||
/// 输出升级状态 |
|||
final int? type; |
|||
|
|||
/// 输出升级进度 |
|||
final double? progress; |
|||
|
|||
/// 错误码 (仅在发生错误时有效) |
|||
final int? code; |
|||
|
|||
/// 错误信息 (仅在发生错误时有效) |
|||
final String? message; |
|||
|
|||
/// 构造函数 |
|||
OtaEvent({ |
|||
required this.event, |
|||
this.type, |
|||
this.progress, |
|||
this.code, |
|||
this.message, |
|||
}); |
|||
|
|||
/// 从Map构造对象 |
|||
factory OtaEvent.fromMap(Map<dynamic, dynamic> map) { |
|||
return OtaEvent( |
|||
event: map['event'], |
|||
type: map['type'], |
|||
progress: map['progress'], |
|||
code: map['code'], |
|||
message: map['message'], |
|||
); |
|||
} |
|||
} |
|||
|
|||
class Ota { |
|||
Future<String?> getPlatformVersion() { |
|||
return OtaPlatform.instance.getPlatformVersion(); |
|||
// 单例实例 |
|||
static final Ota _instance = Ota._internal(); |
|||
|
|||
// 私有构造函数 |
|||
Ota._internal() { |
|||
// 初始化时设置事件监听 |
|||
// _setupEventListener(); |
|||
} |
|||
|
|||
// 公共访问点 |
|||
static Ota get instance => _instance; |
|||
|
|||
static const EventChannel _eventChannel = EventChannel( |
|||
'com.example.ota/events', |
|||
); |
|||
// double progress = 0.0; |
|||
// late int type = OtaEvent.NOT_STARTED; |
|||
// bool isUpgrade = false; |
|||
|
|||
/// 事件流 |
|||
Stream<OtaEvent>? _eventStream; |
|||
// bool _isEventListenerSet = false; // 防止重复监听 |
|||
|
|||
/// 获取事件流 |
|||
Stream<OtaEvent> get eventStream { |
|||
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) { |
|||
if (event is Map) { |
|||
return OtaEvent.fromMap(event); |
|||
} |
|||
throw PlatformException(code: 'INVALID_EVENT', message: '无效的事件数据'); |
|||
}); |
|||
return _eventStream!; |
|||
} |
|||
|
|||
Future<String?> searchDevices() { |
|||
return OtaPlatform.instance.searchDevices(); |
|||
/// 释放资源 |
|||
void onClose() { |
|||
onOtaClose(); |
|||
// 停止事件监听 |
|||
_eventStream = null; |
|||
//_isEventListenerSet = false; |
|||
print("Ota 对象资源已释放"); |
|||
} |
|||
|
|||
Future<String?> connectDevice() { |
|||
return OtaPlatform.instance.connectDevice(); |
|||
// /// 内部设置事件监听(只执行一次) |
|||
// void _setupEventListener() { |
|||
// if (_isEventListenerSet) return; |
|||
|
|||
// eventStream.listen((event) { |
|||
// if (event.event == 'onProgress' && |
|||
// event.type != null && |
|||
// event.progress != null) { |
|||
// print("flutter 收到数据${event.progress}type=${type}"); |
|||
// progress = event.progress!; |
|||
// type = event.type!; |
|||
// } |
|||
// if (event.event == 'onStartOTA') { |
|||
// type = OtaEvent.START_UPGRADING; |
|||
// isUpgrade = true; // 开始升级 |
|||
// } |
|||
// if (event.event == 'onStopOTA') { |
|||
// type = OtaEvent.UPGRADE_SUCCESS; |
|||
// isUpgrade = false; |
|||
// } |
|||
// if (event.event == 'onError' && |
|||
// event.code != null && |
|||
// event.message != null) { |
|||
// print("flutter onError${event.code}type=${event.message}"); |
|||
// type = OtaEvent.UPGRADE_FAILED; |
|||
// isUpgrade = false; |
|||
// } |
|||
// if (event.event == 'NOCONNECTION') { |
|||
// type = OtaEvent.CONNECTION_DISCONNECTION; |
|||
// isUpgrade = false; |
|||
// } |
|||
// if (event.event == 'CONNECTION_SUCCESS') { |
|||
// type = OtaEvent.CONNECTION_SUCCESS; |
|||
// } |
|||
// }); |
|||
|
|||
// _isEventListenerSet = true; |
|||
// } |
|||
|
|||
Future<String?> startUpgrading(String? filePath) { |
|||
return OtaPlatform.instance.startUpgrading(filePath); |
|||
} |
|||
|
|||
Future<String?> startUpgrade() { |
|||
return OtaPlatform.instance.startUpgrade(); |
|||
Future<String?> onOtaClose() { |
|||
return OtaPlatform.instance.onOtaClose(); |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue