Browse Source

上传用户画像以及定位服务优化

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
85868dc0e8
  1. 1
      android/app/src/main/AndroidManifest.xml
  2. 4
      lib/core/bindings/initial_binding.dart
  3. 2
      lib/data/services/location_manager.dart
  4. 2
      lib/data/services/network/nw_api.dart
  5. 30
      lib/data/services/user_portrait.dart
  6. 9
      lib/modules/profile/controllers/profile_controller.dart
  7. 66
      lib/modules/profile/views/profile_view.dart
  8. 1
      local_plugins/agent_service/android/build.gradle.kts
  9. 19
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  10. 11
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  11. 5
      local_plugins/chat_api/android/build.gradle.kts
  12. 208
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  13. 434
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt
  14. 39
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt
  15. 57
      local_plugins/location_service/lib/location_service.dart
  16. 33
      local_plugins/user_portrait/.gitignore
  17. 33
      local_plugins/user_portrait/.metadata
  18. 3
      local_plugins/user_portrait/CHANGELOG.md
  19. 1
      local_plugins/user_portrait/LICENSE
  20. 15
      local_plugins/user_portrait/README.md
  21. 4
      local_plugins/user_portrait/analysis_options.yaml
  22. 9
      local_plugins/user_portrait/android/.gitignore
  23. 66
      local_plugins/user_portrait/android/build.gradle
  24. 1
      local_plugins/user_portrait/android/settings.gradle
  25. 3
      local_plugins/user_portrait/android/src/main/AndroidManifest.xml
  26. 90
      local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt
  27. 45
      local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt
  28. 38
      local_plugins/user_portrait/ios/.gitignore
  29. 29
      local_plugins/user_portrait/ios/user_portrait.podspec
  30. 32
      local_plugins/user_portrait/ios/user_portrait/Package.swift
  31. 14
      local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy
  32. 19
      local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift
  33. 26
      local_plugins/user_portrait/lib/user_portrait.dart
  34. 72
      local_plugins/user_portrait/pubspec.yaml
  35. 27
      local_plugins/user_portrait/test/user_portrait_method_channel_test.dart
  36. 29
      local_plugins/user_portrait/test/user_portrait_test.dart
  37. 2
      pubspec.yaml

1
android/app/src/main/AndroidManifest.xml

@ -112,7 +112,6 @@
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. 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. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>

4
lib/core/bindings/initial_binding.dart

@ -1,6 +1,7 @@
import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/location_manager.dart';
import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/data/services/user_portrait.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../data/services/meeting/meeting_upload_service.dart'; import '../../data/services/meeting/meeting_upload_service.dart';
import '../../data/services/speech_factory.dart'; import '../../data/services/speech_factory.dart';
@ -55,6 +56,9 @@ class InitialBinding extends Bindings {
Get.lazyPut<MeetingUploadService>(() => MeetingUploadService(), Get.lazyPut<MeetingUploadService>(() => MeetingUploadService(),
fenix: true); fenix: true);
// 会议助手音频上传
Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true);
// 杰理蓝牙服务 // 杰理蓝牙服务
// // 语言管理器(需要最先初始化) // // 语言管理器(需要最先初始化)

2
lib/data/services/location_manager.dart

@ -45,7 +45,7 @@ class LocationManager extends GetxService {
} }
//定位 //定位
Future<Map<String, double>> location() async { Future<Map<String, dynamic>> location() async {
return await _musicService.location(); return await _musicService.location();
} }
} }

2
lib/data/services/network/nw_api.dart

@ -1,5 +1,5 @@
class NWApi { class NWApi {
// static const baseApi = "https://www.deapsound.com"; // static const baseApi = "https://www.deapsound.com";
static const baseApi = "https://api.ideapsound.com"; static const baseApi = "https://api.deapsound.com";
// static const baseApi = "https://api.ideapsound.com"; // static const baseApi = "https://api.ideapsound.com";
} }

30
lib/data/services/user_portrait.dart

@ -0,0 +1,30 @@
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
import 'package:user_portrait/user_portrait.dart';
//音乐播放管理
class UserportraitManager extends GetxService {
static UserportraitManager get to => Get.find<UserportraitManager>();
// 音乐服务实例
final UserPortrait _userPortrait = UserPortrait();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('初始化权限请求');
}
/// 释放资源
@override
void onClose() {
super.onClose();
}
//定位
Future<void> clean() async {
_userPortrait.clean();
return;
}
}

9
lib/modules/profile/controllers/profile_controller.dart

@ -1,3 +1,4 @@
import 'package:deep_voice/data/services/user_portrait.dart';
import 'package:dio/dio.dart' as dio; import 'package:dio/dio.dart' as dio;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@ -30,7 +31,8 @@ class ProfileController extends GetxController {
final RxBool hasEdits = false.obs; // 是否有编辑 final RxBool hasEdits = false.obs; // 是否有编辑
final RxBool isUpdating = false.obs; // 是否正在更新用户信息 final RxBool isUpdating = false.obs; // 是否正在更新用户信息
final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态)
//音乐播放管理
final _userportraitManager = Get.find<UserportraitManager>();
// 用于乐观更新的原始状态备份 // 用于乐观更新的原始状态备份
String _originalName = ''; String _originalName = '';
String _originalAvatar = ''; String _originalAvatar = '';
@ -648,6 +650,11 @@ class ProfileController extends GetxController {
} }
} }
// 清理用户缓存
Future<void> clean() async {
await _userportraitManager.clean();
}
// 退出登录 // 退出登录
Future<void> signOut() async { Future<void> signOut() async {
try { try {

66
lib/modules/profile/views/profile_view.dart

@ -730,6 +730,72 @@ class EditProfileView extends GetView<ProfileController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 清理缓存
_buildSimpleNavigationSetting(
title: 'clean'.tr, // 清理缓存
icon: Icons.logout,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
onTap: () {
// 退出登录确认对话框
Get.dialog(
AlertDialog(
backgroundColor:
isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'clean'.tr, // 清理缓存
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'cleanTips'.tr, // 确定要退出当前账号吗?
style: TextStyle(
color:
isDarkMode ? Colors.white70 : Colors.black54,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode
? Colors.white70
: Colors.black54,
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.clean();
},
child: Text(
'confirm'.tr, // 确定
style: TextStyle(
color: isDarkMode
? Colors.red[300]
: Colors.red[600],
),
),
),
],
),
);
},
isDarkMode: isDarkMode,
),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode

1
local_plugins/agent_service/android/build.gradle.kts

@ -51,4 +51,5 @@ dependencies {
add("compileOnly", project(":chat_api")) add("compileOnly", project(":chat_api"))
add("compileOnly", project(":location_service")) add("compileOnly", project(":location_service"))
add("compileOnly", project(":bytedance_speech")) add("compileOnly", project(":bytedance_speech"))
add("compileOnly", project(":user_portrait"))
} }

19
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -11,6 +11,7 @@ import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceHelper import com.yunqiinnovation.amap_service.NavigationServiceHelper
import com.yunqiinnovation.user_portrait.UserPortrait
import com.yunqiinnovation.location_service.LocationService import com.yunqiinnovation.location_service.LocationService
import android.util.Log import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -166,6 +167,8 @@ object AgentService : CoroutineScope {
this.chatApiService = ChatApiService(context) this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context) this.audioPlayer = AudioPlayer(context)
//初始化用户画像
UserPortrait.initialize(appContext);
//初始化定位服务 //初始化定位服务
LocationService.initialize(appContext) LocationService.initialize(appContext)
// 设置系统提示词 // 设置系统提示词
@ -791,14 +794,16 @@ object AgentService : CoroutineScope {
// 先添加系统提示词 // 先添加系统提示词
if (systemPrompt.isNotEmpty()) { if (systemPrompt.isNotEmpty()) {
val location = LocationService.getCachedLocation() ?: mapOf( // val location = LocationService.getCachedLocation() ?: mapOf(
"user_location" to "定位未开启", // "user_location" to "定位未开启",
"user_lat" to "定位未开启", // "user_lat" to "定位未开启",
"user_lng" to "定位未开启", // "user_lng" to "定位未开启",
) // )
val _systemPrompt = Utils.fillTemplate(systemPrompt, location) val mapdata = UserPortrait.getAll()
Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
// Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}")
} }
// 再添加历史消息 // 再添加历史消息

11
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

@ -142,14 +142,11 @@ object Utils {
} }
} }
/**
* 替换字符串 变量
* */
fun fillTemplate(tpl: String, data: Map<String, String>): String { fun fillTemplate(tpl: String, data: Map<String, String>): String {
var result = tpl val regex = Regex("\\{([a-zA-Z0-9_]+)\\}")
data.forEach { (k, v) -> return regex.replace(tpl) { matchResult ->
result = result.replace("{$k}", v) val key = matchResult.groupValues[1]
data[key] ?: "未知"
} }
return result
} }
} }

5
local_plugins/chat_api/android/build.gradle.kts

@ -57,4 +57,7 @@ dependencies {
// JSON处理 // JSON处理
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
}
add("compileOnly", project(":location_service"))
}

208
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -18,6 +18,7 @@ import android.provider.Settings
import android.telephony.SmsManager import android.telephony.SmsManager
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.yunqiinnovation.location_service.LocationService
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -141,16 +142,16 @@ class SystemFunctionHandler(private val context: Context? = null) {
) )
// 注册获取当前位置函数 // 注册获取当前位置函数
// client.registerLocalFunction( client.registerLocalFunction(
// name = "get_current_location", name = "get_current_location",
// description = "获取当前地理位置", description = "获取当前地理位置",
// parameters = mapOf( parameters = mapOf(
// "type" to "object", "type" to "object",
// "properties" to emptyMap<String, Any>(), "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>() "required" to emptyList<String>()
// ), ),
// handler = GetCurrentLocationHandler(context) handler = GetCurrentLocationHandler(context)
// ) )
// 注册媒体播放功能 // 注册媒体播放功能
client.registerLocalFunction( client.registerLocalFunction(
@ -269,6 +270,32 @@ class SystemFunctionHandler(private val context: Context? = null) {
), ),
handler = MuteVolumeHandler(context) handler = MuteVolumeHandler(context)
) )
// 设置最大音量
client.registerLocalFunction(
name = "max_volume",
description = "设置最大音量",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MaxVolumeHandler(context)
)
// 更新用户画像
client.registerLocalFunction(
name = "set_user_profile_field",
description = "设置或更新用户画像中的某一个字段",
parameters = mapOf(
"type" to "object",
"properties" to mapOf(
"key" to mapOf("type" to "string","description" to "画像字段名,例如:age、tone、工作经历、device_preference"),
"value" to mapOf("type" to "string","description" to "字段对应的值"),
),
"required" to listOf("key","value"),
),
handler = UpdateUserPortraitHandler(context)
)
} }
/** /**
@ -1107,6 +1134,7 @@ private class GetCurrentTimeHandler : FunctionHandler {
* 获取当前位置处理器 * 获取当前位置处理器
*/ */
private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler { private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) { if (context == null) {
return JSONObject().apply { return JSONObject().apply {
@ -1116,9 +1144,9 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
}) })
}.toString() }.toString()
} }
val handler = context.let { SystemFunctionHandler(it) } val handler = context.let { SystemFunctionHandler(it) }
// 检查位置权限 // 检查位置权限
if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) { if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) {
return JSONObject().apply { return JSONObject().apply {
@ -1128,36 +1156,84 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
}) })
}.toString() }.toString()
} }
// 获取缓存的位置数据
val cachedLocation = LocationService.getCachedLocationData()
// 检查位置服务是否启用 // 异步触发最新定位获取(不等待结果)
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager LocationService.requestLocationInBackground(object : LocationService.LocationCallback {
val isLocationEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true Log.d(SystemFunctionHandler.TAG, "后台定位成功,缓存已更新: $latitude, $longitude, $address")
if (!isLocationEnabled) {
// 尝试引导用户打开位置服务
try {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "打开位置设置失败: ${e.message}", e)
} }
override fun onLocationError(errorCode: Int, errorMessage: String) {
Log.w(SystemFunctionHandler.TAG, "后台定位失败: $errorCode - $errorMessage")
}
})
// 立即返回缓存数据(如果存在)
if (cachedLocation != null) {
val cacheAge = System.currentTimeMillis() - cachedLocation.timestamp
val cacheAgeMinutes = cacheAge / (60 * 1000)
return JSONObject().apply { return JSONObject().apply {
put("error", JSONObject().apply { put("result", JSONObject().apply {
put("code", "location_disabled") put("latitude", cachedLocation.latitude)
put("message", "位置服务未启用,请打开GPS或网络定位") put("longitude", cachedLocation.longitude)
put("address", cachedLocation.address)
put("source", "cache")
put("timestamp", cachedLocation.timestamp)
put("cacheAgeMinutes", cacheAgeMinutes)
put("note", "正在后台更新位置信息")
}) })
}.toString() }.toString()
} }
// 注意:这里简化了实现,实际应该获取真实位置 // 如果没有缓存,则同步获取一次
// 在生产环境中应该使用 LocationManager 或 FusedLocationProviderClient 获取实际位置 return try {
return JSONObject().apply { kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
put("result", "位置服务已启用,可以获取位置信息") LocationService.requestLocation(object : LocationService.LocationCallback {
put("note", "需要实现实际的位置获取逻辑") override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
}.toString() Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude, $address")
val result = JSONObject().apply {
put("result", JSONObject().apply {
put("latitude", latitude)
put("longitude", longitude)
put("address", address)
put("source", "gps")
put("timestamp", System.currentTimeMillis())
put("note", "首次获取位置信息")
})
}.toString()
continuation.resumeWith(Result.success(result))
}
override fun onLocationError(errorCode: Int, errorMessage: String) {
Log.e(SystemFunctionHandler.TAG, "首次定位失败: $errorCode - $errorMessage")
val result = JSONObject().apply {
put("error", JSONObject().apply {
put("code", "location_error")
put("message", "定位失败: $errorMessage")
put("errorCode", errorCode)
})
}.toString()
continuation.resumeWith(Result.success(result))
}
})
}
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "获取位置时发生异常: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "exception")
put("message", "获取位置时发生异常: ${e.message}")
})
}.toString()
}
} }
} }
@ -1463,3 +1539,67 @@ private class MuteVolumeHandler(private val context: Context?) : FunctionHandler
}.toString() }.toString()
} }
} }
/**
* 添加音量处理类
*/
private class MaxVolumeHandler(private val context: Context?) : FunctionHandler {
//设置音量
fun setMaxMusicVolume(context: Context) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume, AudioManager.FLAG_SHOW_UI)
}
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互
setMaxMusicVolume(context)
return JSONObject().apply {
put("result", "设置静音成功!")
put("broadcast", false)
}.toString()
}
}
/**
* 更新用户画像
*/
private class UpdateUserPortraitHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
val key = arguments["key"]?.toString() ?: "未知"
val value = arguments["value"]?.toString() ?: "未知"
// 发送更新画像广播
val intent = Intent("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT").apply {
putExtra("key", key)
putExtra("value", value)
setPackage(context.packageName)
}
context.sendBroadcast(intent)
// 发送广播通知退出交互
return JSONObject().apply {
put("result", "更新成功!")
}.toString()
}
}

434
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

@ -1,32 +1,52 @@
package com.yunqiinnovation.location_service package com.yunqiinnovation.location_service
import android.Manifest import android.Manifest
import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.amap.api.location.* import com.amap.api.location.*
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object LocationService { object LocationService {
private const val TAG = "LocationService" private const val TAG = "LocationService"
private var appcontext:Context? = null private const val PERMISSION_REQUEST_CODE = 1001
private var appcontext: Context? = null
private var locationClient: AMapLocationClient? = null private var locationClient: AMapLocationClient? = null
private var currentLocation: AMapLocation? = null
private var initialized = false private var initialized = false
private var permissionRetryHandler: Handler? = null private var permissionRetryHandler: Handler? = null
private var permissionRetryRunnable: Runnable? = null private var permissionRetryRunnable: Runnable? = null
// 存储当前的定位请求回调(Flutter用)
private var currentLocationCallback: MethodChannel.Result? = null
// 存储权限请求回调(Flutter用)
private var permissionRequestCallback: MethodChannel.Result? = null
// 存储服务间调用的回调
private val serviceCallbacks = mutableListOf<LocationCallback>()
/**
* 定位回调接口,供其他Android服务使用
*/
interface LocationCallback {
fun onLocationSuccess(latitude: Double, longitude: Double, address: String)
fun onLocationError(errorCode: Int, errorMessage: String)
}
/** /**
* 检查应用是否在前台运行 * 检查应用是否在前台运行
* @return 应用是否在前台
*/ */
fun isAppInForeground(context: Context,): Boolean { fun isAppInForeground(context: Context): Boolean {
try { try {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false val appProcesses = activityManager.runningAppProcesses ?: return false
@ -39,53 +59,154 @@ object LocationService {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApi", "检查应用状态失败: ${e.message}", e) Log.e(TAG, "检查应用状态失败: ${e.message}", e)
} }
return false return false
} }
fun initialize(context: Context) { fun initialize(context: Context) {
if (initialized) return if (initialized) return
appcontext = context appcontext = context
// Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
if (!checkPermissions(context)) { if (!checkPermissions(context)) {
Log.w(TAG, "没有定位权限,等待用户授权中...") Log.w(TAG, "没有定位权限,等待用户授权中...")
startPermissionMonitor() startPermissionMonitor()
return return
} }
try { try {
AMapLocationClient.updatePrivacyShow(context, true, true) AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true) AMapLocationClient.updatePrivacyAgree(context, true)
// Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
locationClient = AMapLocationClient(context.applicationContext) locationClient = AMapLocationClient(context.applicationContext)
val option = AMapLocationClientOption().apply { val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
isOnceLocation = false isOnceLocation = true // 单次定位
interval = 30000L isNeedAddress = true // 需要地址信息
httpTimeOut = 20000 // 设置超时时间
} }
// Log.e(TAG, "liwei--------- 初始化高德定位服务 3")
locationClient?.setLocationOption(option) locationClient?.setLocationOption(option)
// Log.e(TAG, "liwei--------- 初始化高德定位服务 4")
// 设置定位监听器
locationClient?.setLocationListener { location -> locationClient?.setLocationListener { location ->
if (location != null && location.errorCode == 0) { handleLocationResult(location)
currentLocation = location
// Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}")
} else {
Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}")
}
} }
// Log.e(TAG, "liwei--------- 初始化高德定位服务 5")
locationClient?.startLocation()
// Log.e(TAG, "liwei--------- 初始化高德定位服务 6")
initialized = true initialized = true
Log.d(TAG, "定位服务初始化完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "高德定位初始化失败: ${e.message}", e) Log.e(TAG, "高德定位初始化失败: ${e.message}", e)
} }
} }
// 在LocationService object中添加缓存相关的属性和方法
private var cachedLocationData: LocationData? = null
private const val CACHE_PREFS_NAME = "location_cache"
/**
* 后台异步获取位置信息(不阻塞调用者)
* @param callback 定位结果回调
*/
fun requestLocationInBackground(callback: LocationCallback) {
val ctx = appcontext
if (ctx == null) {
callback.onLocationError(-1, "定位服务未初始化")
return
}
if (!checkPermissions(ctx)) {
callback.onLocationError(-2, "没有定位权限")
return
}
if (!initialized) {
callback.onLocationError(-3, "定位服务未初始化")
return
}
// 使用协程在后台执行
CoroutineScope(Dispatchers.IO).launch {
try {
// 添加到回调列表
serviceCallbacks.add(callback)
// 如果没有正在进行的定位请求,开始定位
if (currentLocationCallback == null && serviceCallbacks.size == 1) {
locationClient?.startLocation()
}
} catch (e: Exception) {
callback.onLocationError(-4, "启动后台定位失败: ${e.message}")
}
}
}
// 修改handleLocationResult方法,添加缓存逻辑
private fun handleLocationResult(location: AMapLocation?) {
// 处理Flutter回调
val flutterCallback = currentLocationCallback
currentLocationCallback = null
if (location != null && location.errorCode == 0) {
Log.d(TAG, "定位成功: ${location.latitude}, ${location.longitude}")
// 缓存位置数据
val locationData = LocationData(
latitude = location.latitude,
longitude = location.longitude,
address = location.address ?: "未知地址",
timestamp = System.currentTimeMillis()
)
cachedLocationData = locationData
saveCachedLocationToPrefs(locationData)
// 通知Flutter
if (flutterCallback != null) {
val result = mapOf(
"user_location" to (location.address ?: "未知地址"),
"user_lat" to location.latitude.toString(),
"user_lng" to location.longitude.toString()
)
flutterCallback.success(result)
}
// 通知其他Android服务
serviceCallbacks.forEach { callback ->
try {
callback.onLocationSuccess(
location.latitude,
location.longitude,
location.address ?: "未知地址"
)
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
}
}
} else {
Log.w(TAG, "定位失败: ${location?.errorCode} - ${location?.errorInfo}")
// 通知Flutter
if (flutterCallback != null) {
flutterCallback.error("LOCATION_ERROR", "定位失败: ${location?.errorInfo ?: "未知错误"}", null)
}
// 通知其他Android服务
serviceCallbacks.forEach { callback ->
try {
callback.onLocationError(
location?.errorCode ?: -1,
location?.errorInfo ?: "未知错误"
)
} catch (e: Exception) {
Log.e(TAG, "服务回调失败: ${e.message}", e)
}
}
}
// 清空服务回调列表(单次定位)
serviceCallbacks.clear()
}
private fun startPermissionMonitor() { private fun startPermissionMonitor() {
if (permissionRetryHandler != null) return if (permissionRetryHandler != null) return
@ -98,7 +219,7 @@ object LocationService {
if (hasPermission && !initialized) { if (hasPermission && !initialized) {
Log.d(TAG, "权限已授予,重新初始化定位服务") Log.d(TAG, "权限已授予,重新初始化定位服务")
initialize(ctx) initialize(ctx)
stopPermissionMonitor() // 停止定时器 stopPermissionMonitor()
} else { } else {
permissionRetryHandler?.postDelayed(this, 5000) permissionRetryHandler?.postDelayed(this, 5000)
} }
@ -114,10 +235,10 @@ object LocationService {
} }
private fun checkPermissions(context: Context): Boolean { private fun checkPermissions(context: Context): Boolean {
if (isAppInForeground(context)) { return if (isAppInForeground(context)) {
return hasForegroundLocationPermission(context) hasForegroundLocationPermission(context)
}else{ } else {
return hasBackgroundLocationPermission(context) hasBackgroundLocationPermission(context)
} }
} }
@ -139,58 +260,237 @@ object LocationService {
} }
} }
/**
* 检查定位权限状态(Flutter用)
*/
fun checkLocationPermission(context: Context, result: MethodChannel.Result) {
val hasPermission = checkPermissions(context)
result.success(mapOf(
"hasPermission" to hasPermission,
"hasForegroundPermission" to hasForegroundLocationPermission(context),
"hasBackgroundPermission" to hasBackgroundLocationPermission(context)
))
}
fun getCachedLocation(): Map<String, String>? { /**
// // 定位成功,获取经纬度 * 请求定位权限(Flutter用)
// val latitude = location.latitude */
// val longitude = location.longitude fun requestLocationPermission(activity: Activity?, result: MethodChannel.Result) {
// if (activity == null) {
// // 获取结构化地址 result.error("NO_ACTIVITY", "无法获取Activity实例", null)
// val province = location.province // 省 return
// val city = location.city // 市 }
// val district = location.district // 区县
// val street = location.street // 街道 val ctx = appcontext ?: activity
// val streetNum = location.streetNum // 门牌号
// val adCode = location.adCode // 行政区划代码 // 如果已经有权限,直接返回成功
// val address = location.address // 详细地址拼接字符串 if (checkPermissions(ctx)) {
if (!initialized){ result.success(mapOf("granted" to true))
if (checkPermissions(appcontext!!)) { return
return mapOf(
"user_location" to "当前定位权限为获取",
"user_lat" to "当前定位权限为获取",
"user_lng" to "当前定位权限为获取",
)
}else{
return mapOf(
"user_location" to "后台服务下,需要后台定位权限",
"user_lat" to "后台服务下,需要后台定位权限",
"user_lng" to "后台服务下,需要后台定位权限",
)
}
} }
return currentLocation?.let { // 保存回调
mapOf( permissionRequestCallback = result
"user_location" to it.address,
"user_lat" to it.latitude.toString(), // 请求权限
"user_lng" to it.longitude.toString(), val permissions = mutableListOf<String>()
if (!hasForegroundLocationPermission(ctx)) {
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !hasBackgroundLocationPermission(ctx)) {
permissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
if (permissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
permissions.toTypedArray(),
PERMISSION_REQUEST_CODE
) )
} else {
result.success(mapOf("granted" to true))
} }
} }
fun getCurrentLocation(result: MethodChannel.Result) {
val cached = getCachedLocation() /**
if (cached != null) { * 处理权限请求结果(Flutter用)
result.success(cached) */
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
if (requestCode != PERMISSION_REQUEST_CODE) {
return false
}
val callback = permissionRequestCallback
permissionRequestCallback = null
if (callback == null) {
return true
}
val ctx = appcontext
if (ctx == null) {
callback.error("NO_CONTEXT", "上下文丢失", null)
return true
}
val hasPermission = checkPermissions(ctx)
if (hasPermission) {
// 权限获取成功,重新初始化定位服务
if (!initialized) {
initialize(ctx)
}
callback.success(mapOf("granted" to true))
} else { } else {
result.error("NO_LOCATION", "当前位置还未获取", null) callback.success(mapOf("granted" to false))
} }
return true
} }
/**
* 按需获取当前位置(Flutter用)
*/
fun getCurrentLocation(result: MethodChannel.Result) {
val ctx = appcontext
if (ctx == null) {
result.error("NOT_INITIALIZED", "定位服务未初始化", null)
return
}
if (!checkPermissions(ctx)) {
result.error("NO_PERMISSION", "需要定位权限", null)
return
}
if (!initialized) {
result.error("NOT_INITIALIZED", "定位服务未初始化", null)
return
}
if (currentLocationCallback != null) {
result.error("LOCATION_IN_PROGRESS", "正在定位中,请稍后再试", null)
return
}
// 保存回调,开始定位
currentLocationCallback = result
locationClient?.startLocation()
}
// ========== 以下是供其他Android服务使用的接口 ==========
/**
* 检查是否有定位权限(供其他服务使用)
*/
fun hasLocationPermission(): Boolean {
val ctx = appcontext ?: return false
return checkPermissions(ctx)
}
/**
* 获取当前位置(供其他Android服务使用)
* @param callback 定位结果回调
*/
fun requestLocation(callback: LocationCallback) {
val ctx = appcontext
if (ctx == null) {
callback.onLocationError(-1, "定位服务未初始化")
return
}
if (!checkPermissions(ctx)) {
callback.onLocationError(-2, "没有定位权限")
return
}
if (!initialized) {
callback.onLocationError(-3, "定位服务未初始化")
return
}
// 添加到回调列表
serviceCallbacks.add(callback)
// 如果没有正在进行的定位请求,开始定位
if (currentLocationCallback == null && serviceCallbacks.size == 1) {
locationClient?.startLocation()
}
}
/**
* 同步获取缓存的位置信息(供其他服务使用)
* 注意:这个方法返回的可能是过期的位置信息
*/
fun getCachedLocationData(): LocationData? {
// 优先返回内存中的缓存
if (cachedLocationData != null) {
return cachedLocationData
}
// 从SharedPreferences中读取缓存
val ctx = appcontext ?: return null
return loadCachedLocationFromPrefs(ctx)
}
private fun saveCachedLocationToPrefs(locationData: LocationData) {
val ctx = appcontext ?: return
try {
val prefs = ctx.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
putString("latitude", locationData.latitude.toString())
putString("longitude", locationData.longitude.toString())
putString("address", locationData.address)
putLong("timestamp", locationData.timestamp)
apply()
}
} catch (e: Exception) {
Log.e(TAG, "保存位置缓存失败: ${e.message}", e)
}
}
private fun loadCachedLocationFromPrefs(context: Context): LocationData? {
return try {
val prefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
val latitude = prefs.getString("latitude", null)?.toDoubleOrNull() ?: return null
val longitude = prefs.getString("longitude", null)?.toDoubleOrNull() ?: return null
val address = prefs.getString("address", null) ?: return null
val timestamp = prefs.getLong("timestamp", 0L)
if (timestamp == 0L) return null
LocationData(latitude, longitude, address, timestamp)
} catch (e: Exception) {
Log.e(TAG, "读取位置缓存失败: ${e.message}", e)
null
}
}
/**
* 位置数据类
*/
data class LocationData(
val latitude: Double,
val longitude: Double,
val address: String,
val timestamp: Long
)
fun stop() { fun stop() {
locationClient?.stopLocation() locationClient?.stopLocation()
locationClient?.onDestroy() locationClient?.onDestroy()
locationClient = null locationClient = null
currentLocation = null currentLocationCallback = null
permissionRequestCallback = null
serviceCallbacks.clear()
initialized = false initialized = false
stopPermissionMonitor()
} }
} }

39
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt

@ -3,16 +3,20 @@ package com.yunqiinnovation.location_service
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
/** LocationServicePlugin */ /** LocationServicePlugin */
class LocationServicePlugin: FlutterPlugin, MethodCallHandler { class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener {
private val TAG = "LocationServicePlugin" private val TAG = "LocationServicePlugin"
private lateinit var context: Context private lateinit var context: Context
private lateinit var channel : MethodChannel private lateinit var channel : MethodChannel
private var activityBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
@ -24,9 +28,40 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler {
Log.d(TAG, "Method call: ${call.method}") Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"location" -> LocationService.getCurrentLocation(result) "location" -> LocationService.getCurrentLocation(result)
"requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result)
"checkPermission" -> LocationService.checkLocationPermission(context, result)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addRequestPermissionsResultListener(this)
LocationService.initialize(context)
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
activityBinding?.removeRequestPermissionsResultListener(this)
activityBinding = null
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
return LocationService.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
} }

57
local_plugins/location_service/lib/location_service.dart

@ -14,11 +14,56 @@ class LocationService {
// 内部构造函数 // 内部构造函数
LocationService._internal() {} LocationService._internal() {}
Future<Map<String, double>> location() async { /// 检查定位权限
final result = await _methodChannel.invokeMethod<Map>('location'); Future<Map<String, bool>> checkPermission() async {
return { try {
'latitude': result?['longitude'] ?? 0.0, final result = await _methodChannel.invokeMethod<Map>('checkPermission');
'longitude': result?['longitude'] ?? 0.0, return {
}; 'hasPermission': result?['hasPermission'] ?? false,
'hasForegroundPermission': result?['hasForegroundPermission'] ?? false,
'hasBackgroundPermission': result?['hasBackgroundPermission'] ?? false,
};
} catch (e) {
throw Exception('检查权限失败: $e');
}
}
/// 请求定位权限
Future<bool> requestPermission() async {
try {
final result = await _methodChannel.invokeMethod<Map>(
'requestPermission',
);
return result?['granted'] ?? false;
} catch (e) {
throw Exception('请求权限失败: $e');
}
}
/// 获取当前位置(按需获取)
Future<Map<String, dynamic>> location() async {
try {
// 先检查权限
final permissionStatus = await checkPermission();
if (!permissionStatus['hasPermission']!) {
// 请求权限
final granted = await requestPermission();
if (!granted) {
throw Exception('用户拒绝了定位权限');
}
}
final result = await _methodChannel.invokeMethod<Map>('location');
if (result != null) {
return {
'latitude': double.tryParse(result['user_lat'] ?? '0.0') ?? 0.0,
'longitude': double.tryParse(result['user_lng'] ?? '0.0') ?? 0.0,
'address': result['user_location'] ?? '未知地址',
};
}
throw Exception('获取位置信息失败');
} catch (e) {
throw Exception('定位失败: $e');
}
} }
} }

33
local_plugins/user_portrait/.gitignore

@ -0,0 +1,33 @@
# 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/

33
local_plugins/user_portrait/.metadata

@ -0,0 +1,33 @@
# 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: "5c1433509f997e9059d230942bdf83f1b88a179e"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
- platform: android
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
- platform: ios
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
# 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'

3
local_plugins/user_portrait/CHANGELOG.md

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
local_plugins/user_portrait/LICENSE

@ -0,0 +1 @@
TODO: Add your license here.

15
local_plugins/user_portrait/README.md

@ -0,0 +1,15 @@
# user_portrait
用户画像
## 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.

4
local_plugins/user_portrait/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
local_plugins/user_portrait/android/.gitignore

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

66
local_plugins/user_portrait/android/build.gradle

@ -0,0 +1,66 @@
group = "com.yunqiinnovation.user_portrait"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.1.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.3")
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.user_portrait"
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 {
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
}
}
}
}

1
local_plugins/user_portrait/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'user_portrait'

3
local_plugins/user_portrait/android/src/main/AndroidManifest.xml

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.user_portrait">
</manifest>

90
local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt

@ -0,0 +1,90 @@
package com.yunqiinnovation.user_portrait
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.location.LocationManager
import android.os.Build
import android.provider.Settings
import android.util.Log
import kotlinx.coroutines.SupervisorJob
import org.json.JSONObject
/** UserPortraitPlugin */
object UserPortrait {
private const val TAG = "UserPortrait"
private const val PREF_NAME = "deep_voice_user"
private var appcontext:Context? = null
private var prefs:SharedPreferences? = null
private var portrait: Map<String, Any?> = emptyMap()
/**
* 初始化语音交互代理
*/
fun initialize(context: Context) {
try {
// 注册广播监听器
val filter = IntentFilter().apply {
addAction("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT")
}
// Android 14+ 需要指定 flag
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(controlReceiver, filter)
}
// 获取SharedPreferences
appcontext = context;
prefs = appcontext?.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
portrait = prefs!!.all
} catch (e: Exception) {
Log.e(TAG, "初始化语音交互代理时发生异常", e)
}
}
private val controlReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.i(TAG, "接受到通知事件 ${intent?.action}")
when (intent?.action) {
"com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT" -> {
val key = intent.getStringExtra("key").toString()
val value = intent.getStringExtra("value").toString()
Log.i(TAG, "收到 UPDATE_PORTRAIT 广播 $key:$value")
if (key.isNotEmpty()) {
Log.i(TAG ,"接收到画像更新:$key = $value")
set(key, value)
}
}
// 你也可以扩展支持更多指令
}
}
}
fun getAll(): Map<String, String> {
return portrait
.filterValues { it != null } // 可选,过滤掉 null
.mapValues { it.value.toString() }
}
//设置画像字段
fun set(key:String,value:String){
val editor = prefs?.edit()
editor?.putString(key, value)
editor?.apply() // 异步提交(推荐)
portrait = portrait.toMutableMap().apply {
this[key] = value
}
}
fun clean(){
portrait = emptyMap();
prefs?.edit()?.clear()?.apply()
}
}

45
local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt

@ -0,0 +1,45 @@
package com.yunqiinnovation.user_portrait
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat.registerReceiver
import io.flutter.embedding.engine.plugins.FlutterPlugin
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
/** UserPortraitPlugin */
class UserPortraitPlugin: 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
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.user_portrait")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "clean") {
UserPortrait.clean()
result.success(null)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

38
local_plugins/user_portrait/ios/.gitignore

@ -0,0 +1,38 @@
.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

29
local_plugins/user_portrait/ios/user_portrait.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint user_portrait.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'user_portrait'
s.version = '0.0.1'
s.summary = '用户画像'
s.description = <<-DESC
用户画像
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'user_portrait/Sources/user_portrait/**/*'
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 = {'user_portrait_privacy' => ['user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy']}
end

32
local_plugins/user_portrait/ios/user_portrait/Package.swift

@ -0,0 +1,32 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "user_portrait",
platforms: [
.iOS("12.0")
],
products: [
.library(name: "user-portrait", targets: ["user_portrait"])
],
dependencies: [],
targets: [
.target(
name: "user_portrait",
dependencies: [],
resources: [
// 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 these lines. For more information, see
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
// .process("PrivacyInfo.xcprivacy"),
// If you have other resources that need to be bundled with your plugin, refer to
// the following instructions to add them:
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
]
)
]
)

14
local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?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>

19
local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift

@ -0,0 +1,19 @@
import Flutter
import UIKit
public class UserPortraitPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "user_portrait", binaryMessenger: registrar.messenger())
let instance = UserPortraitPlugin()
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)
}
}
}

26
local_plugins/user_portrait/lib/user_portrait.dart

@ -0,0 +1,26 @@
import 'package:flutter/services.dart';
class UserPortrait {
static final UserPortrait _instance = UserPortrait._internal();
/// 获取单例
factory UserPortrait() => _instance;
// 方法通道
final MethodChannel _methodChannel = MethodChannel(
'com.yunqiinnovation.user_portrait',
);
// 内部构造函数
UserPortrait._internal() {}
/// 请求定位权限
Future<void> clean() async {
try {
await _methodChannel.invokeMethod<void>('clean');
return;
} catch (e) {
throw Exception('请求权限失败: $e');
}
}
}

72
local_plugins/user_portrait/pubspec.yaml

@ -0,0 +1,72 @@
name: user_portrait
description: "用户画像"
version: 0.0.1
homepage:
environment:
sdk: ^3.8.1
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.user_portrait
pluginClass: UserPortraitPlugin
ios:
pluginClass: UserPortraitPlugin
# 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

27
local_plugins/user_portrait/test/user_portrait_method_channel_test.dart

@ -0,0 +1,27 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:user_portrait/user_portrait_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelUserPortrait platform = MethodChannelUserPortrait();
const MethodChannel channel = MethodChannel('user_portrait');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}

29
local_plugins/user_portrait/test/user_portrait_test.dart

@ -0,0 +1,29 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:user_portrait/user_portrait.dart';
import 'package:user_portrait/user_portrait_platform_interface.dart';
import 'package:user_portrait/user_portrait_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockUserPortraitPlatform
with MockPlatformInterfaceMixin
implements UserPortraitPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
void main() {
final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance;
test('$MethodChannelUserPortrait is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelUserPortrait>());
});
test('getPlatformVersion', () async {
UserPortrait userPortraitPlugin = UserPortrait();
MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform();
UserPortraitPlatform.instance = fakePlatform;
expect(await userPortraitPlugin.getPlatformVersion(), '42');
});
}

2
pubspec.yaml

@ -105,6 +105,8 @@ dependencies:
path: local_plugins/realtime path: local_plugins/realtime
jl_opus: jl_opus:
path: local_plugins/jl_opus path: local_plugins/jl_opus
user_portrait:
path: local_plugins/user_portrait
test: test:
path: local_plugins/test path: local_plugins/test

Loading…
Cancel
Save