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:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>

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/music_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 '../../data/services/meeting/meeting_upload_service.dart';
import '../../data/services/speech_factory.dart';
@ -55,6 +56,9 @@ class InitialBinding extends Bindings {
Get.lazyPut<MeetingUploadService>(() => MeetingUploadService(),
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();
}
}

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

@ -1,5 +1,5 @@
class NWApi {
// 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";
}

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

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

@ -730,6 +730,72 @@ class EditProfileView extends GetView<ProfileController> {
},
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(
height: 1,
color: isDarkMode

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

@ -51,4 +51,5 @@ dependencies {
add("compileOnly", project(":chat_api"))
add("compileOnly", project(":location_service"))
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.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceHelper
import com.yunqiinnovation.user_portrait.UserPortrait
import com.yunqiinnovation.location_service.LocationService
import android.util.Log
import kotlinx.coroutines.*
@ -166,6 +167,8 @@ object AgentService : CoroutineScope {
this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
//初始化用户画像
UserPortrait.initialize(appContext);
//初始化定位服务
LocationService.initialize(appContext)
// 设置系统提示词
@ -791,14 +794,16 @@ object AgentService : CoroutineScope {
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
val location = LocationService.getCachedLocation() ?: mapOf(
"user_location" to "定位未开启",
"user_lat" to "定位未开启",
"user_lng" to "定位未开启",
)
val _systemPrompt = Utils.fillTemplate(systemPrompt, location)
// val location = LocationService.getCachedLocation() ?: mapOf(
// "user_location" to "定位未开启",
// "user_lat" to "定位未开启",
// "user_lng" to "定位未开启",
// )
val mapdata = UserPortrait.getAll()
Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
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 {
var result = tpl
data.forEach { (k, v) ->
result = result.replace("{$k}", v)
val regex = Regex("\\{([a-zA-Z0-9_]+)\\}")
return regex.replace(tpl) { matchResult ->
val key = matchResult.groupValues[1]
data[key] ?: "未知"
}
return result
}
}

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

@ -57,4 +57,7 @@ dependencies {
// JSON处理
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
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.util.Log
import androidx.core.content.ContextCompat
import com.yunqiinnovation.location_service.LocationService
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import org.json.JSONObject
import java.text.SimpleDateFormat
@ -141,16 +142,16 @@ class SystemFunctionHandler(private val context: Context? = null) {
)
// 注册获取当前位置函数
// client.registerLocalFunction(
// name = "get_current_location",
// description = "获取当前地理位置",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = GetCurrentLocationHandler(context)
// )
client.registerLocalFunction(
name = "get_current_location",
description = "获取当前地理位置",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = GetCurrentLocationHandler(context)
)
// 注册媒体播放功能
client.registerLocalFunction(
@ -269,6 +270,32 @@ class SystemFunctionHandler(private val context: Context? = null) {
),
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 {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
@ -1116,9 +1144,9 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
})
}.toString()
}
val handler = context.let { SystemFunctionHandler(it) }
// 检查位置权限
if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) {
return JSONObject().apply {
@ -1128,36 +1156,84 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
})
}.toString()
}
// 获取缓存的位置数据
val cachedLocation = LocationService.getCachedLocationData()
// 检查位置服务是否启用
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
val isLocationEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true ||
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true
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)
// 异步触发最新定位获取(不等待结果)
LocationService.requestLocationInBackground(object : LocationService.LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
Log.d(SystemFunctionHandler.TAG, "后台定位成功,缓存已更新: $latitude, $longitude, $address")
}
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 {
put("error", JSONObject().apply {
put("code", "location_disabled")
put("message", "位置服务未启用,请打开GPS或网络定位")
put("result", JSONObject().apply {
put("latitude", cachedLocation.latitude)
put("longitude", cachedLocation.longitude)
put("address", cachedLocation.address)
put("source", "cache")
put("timestamp", cachedLocation.timestamp)
put("cacheAgeMinutes", cacheAgeMinutes)
put("note", "正在后台更新位置信息")
})
}.toString()
}
// 注意:这里简化了实现,实际应该获取真实位置
// 在生产环境中应该使用 LocationManager 或 FusedLocationProviderClient 获取实际位置
return JSONObject().apply {
put("result", "位置服务已启用,可以获取位置信息")
put("note", "需要实现实际的位置获取逻辑")
}.toString()
// 如果没有缓存,则同步获取一次
return try {
kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
LocationService.requestLocation(object : LocationService.LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) {
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()
}
}
/**
* 添加音量处理类
*/
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
import android.Manifest
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.amap.api.location.*
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object 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 currentLocation: AMapLocation? = null
private var initialized = false
private var permissionRetryHandler: Handler? = 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 {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
@ -39,53 +59,154 @@ object LocationService {
}
}
} catch (e: Exception) {
Log.e("ChatApi", "检查应用状态失败: ${e.message}", e)
Log.e(TAG, "检查应用状态失败: ${e.message}", e)
}
return false
}
fun initialize(context: Context) {
if (initialized) return
appcontext = context
// Log.e(TAG, "liwei--------- 初始化高德定位服务 1")
if (!checkPermissions(context)) {
Log.w(TAG, "没有定位权限,等待用户授权中...")
startPermissionMonitor()
return
}
try {
AMapLocationClient.updatePrivacyShow(context, true, true)
AMapLocationClient.updatePrivacyAgree(context, true)
// Log.e(TAG, "liwei--------- 初始化高德定位服务 2")
locationClient = AMapLocationClient(context.applicationContext)
val option = AMapLocationClientOption().apply {
locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
isOnceLocation = false
interval = 30000L
isOnceLocation = true // 单次定位
isNeedAddress = true // 需要地址信息
httpTimeOut = 20000 // 设置超时时间
}
// Log.e(TAG, "liwei--------- 初始化高德定位服务 3")
locationClient?.setLocationOption(option)
// Log.e(TAG, "liwei--------- 初始化高德定位服务 4")
// 设置定位监听器
locationClient?.setLocationListener { location ->
if (location != null && location.errorCode == 0) {
currentLocation = location
// Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}")
} else {
Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}")
}
handleLocationResult(location)
}
// Log.e(TAG, "liwei--------- 初始化高德定位服务 5")
locationClient?.startLocation()
// Log.e(TAG, "liwei--------- 初始化高德定位服务 6")
initialized = true
Log.d(TAG, "定位服务初始化完成")
} catch (e: Exception) {
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() {
if (permissionRetryHandler != null) return
@ -98,7 +219,7 @@ object LocationService {
if (hasPermission && !initialized) {
Log.d(TAG, "权限已授予,重新初始化定位服务")
initialize(ctx)
stopPermissionMonitor() // 停止定时器
stopPermissionMonitor()
} else {
permissionRetryHandler?.postDelayed(this, 5000)
}
@ -114,10 +235,10 @@ object LocationService {
}
private fun checkPermissions(context: Context): Boolean {
if (isAppInForeground(context)) {
return hasForegroundLocationPermission(context)
}else{
return hasBackgroundLocationPermission(context)
return if (isAppInForeground(context)) {
hasForegroundLocationPermission(context)
} else {
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>? {
// // 定位成功,获取经纬度
// val latitude = location.latitude
// val longitude = location.longitude
//
// // 获取结构化地址
// val province = location.province // 省
// val city = location.city // 市
// val district = location.district // 区县
// val street = location.street // 街道
// val streetNum = location.streetNum // 门牌号
// val adCode = location.adCode // 行政区划代码
// val address = location.address // 详细地址拼接字符串
if (!initialized){
if (checkPermissions(appcontext!!)) {
return mapOf(
"user_location" to "当前定位权限为获取",
"user_lat" to "当前定位权限为获取",
"user_lng" to "当前定位权限为获取",
)
}else{
return mapOf(
"user_location" to "后台服务下,需要后台定位权限",
"user_lat" to "后台服务下,需要后台定位权限",
"user_lng" to "后台服务下,需要后台定位权限",
)
}
/**
* 请求定位权限(Flutter用)
*/
fun requestLocationPermission(activity: Activity?, result: MethodChannel.Result) {
if (activity == null) {
result.error("NO_ACTIVITY", "无法获取Activity实例", null)
return
}
val ctx = appcontext ?: activity
// 如果已经有权限,直接返回成功
if (checkPermissions(ctx)) {
result.success(mapOf("granted" to true))
return
}
return currentLocation?.let {
mapOf(
"user_location" to it.address,
"user_lat" to it.latitude.toString(),
"user_lng" to it.longitude.toString(),
// 保存回调
permissionRequestCallback = result
// 请求权限
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) {
result.success(cached)
/**
* 处理权限请求结果(Flutter用)
*/
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 {
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() {
locationClient?.stopLocation()
locationClient?.onDestroy()
locationClient = null
currentLocation = null
currentLocationCallback = null
permissionRequestCallback = null
serviceCallbacks.clear()
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.util.Log
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.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
/** LocationServicePlugin */
class LocationServicePlugin: FlutterPlugin, MethodCallHandler {
class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener {
private val TAG = "LocationServicePlugin"
private lateinit var context: Context
private lateinit var channel : MethodChannel
private var activityBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
@ -24,9 +28,40 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler {
Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"location" -> LocationService.getCurrentLocation(result)
"requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result)
"checkPermission" -> LocationService.checkLocationPermission(context, result)
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() {}
Future<Map<String, double>> location() async {
final result = await _methodChannel.invokeMethod<Map>('location');
return {
'latitude': result?['longitude'] ?? 0.0,
'longitude': result?['longitude'] ?? 0.0,
};
/// 检查定位权限
Future<Map<String, bool>> checkPermission() async {
try {
final result = await _methodChannel.invokeMethod<Map>('checkPermission');
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
jl_opus:
path: local_plugins/jl_opus
user_portrait:
path: local_plugins/user_portrait
test:
path: local_plugins/test

Loading…
Cancel
Save