import 'dart:convert'; import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:eitc_erm_dental_flutter/app_router.gr.dart'; import 'package:eitc_erm_dental_flutter/dialog/app_update_dialog.dart'; import 'package:eitc_erm_dental_flutter/entity/file_prefix_info.dart'; import 'package:eitc_erm_dental_flutter/entity/version_info.dart'; import 'package:eitc_erm_dental_flutter/exts.dart'; import 'package:eitc_erm_dental_flutter/generated/l10n.dart'; import 'package:eitc_erm_dental_flutter/http/api_service.dart'; import 'package:eitc_erm_dental_flutter/http/http.dart'; import 'package:eitc_erm_dental_flutter/main.dart'; import 'package:eitc_erm_dental_flutter/sp_util.dart'; import 'package:eitc_erm_dental_flutter/vm/global_view_model.dart'; import 'package:encrypt/encrypt.dart' as encrpyt; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:logger/logger.dart'; import 'package:network_info_plus/network_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointycastle/asymmetric/pkcs1.dart'; import 'package:pointycastle/asymmetric/rsa.dart'; import 'package:pointycastle/pointycastle.dart'; import 'global.dart'; ///国际化 S getS() => S.of(navigatorKey.currentState!.context); late Logger logger; ///初始化日志 Future initLog() async { LogOutput? output; Directory? dir = await getDownloadsDirectory(); if (dir != null) { dir = Directory("${dir.path}/logs"); if (!await dir.exists()) { await dir.create(recursive: true); } output = kDebugMode ? MultiOutput([ConsoleOutput(), AdvancedFileOutput(path: dir.path)]) : ConsoleOutput(); } logger = Logger( printer: PrettyPrinter( methodCount: 5, dateTimeFormat: DateTimeFormat.dateAndTime), output: output); } void logd( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, }) { logger.d(message, time: time ?? DateTime.now(), error: error, stackTrace: stackTrace); } void logi( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, }) { logger.i(message, time: time ?? DateTime.now(), error: error, stackTrace: stackTrace); } void loge( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, }) { logger.e(message, time: time ?? DateTime.now(), error: error, stackTrace: stackTrace); } void logw( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, }) { logger.w(message, time: time ?? DateTime.now(), error: error, stackTrace: stackTrace); } void logf( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, }) { logger.f(message, time: time ?? DateTime.now(), error: error, stackTrace: stackTrace); } ///显示toast void showToast( {required String text, Duration duration = const Duration(seconds: 1, milliseconds: 500)}) { BotToast.showText(text: text, duration: duration); } ///显示删除提示框 Future showDeleteAlertDialog(BuildContext context, String hint) { return showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(getS().hint), content: Text(hint), actions: [ TextButton( onPressed: () => {Navigator.pop(context)}, child: Text(getS().cancel)), TextButton( onPressed: () => {Navigator.pop(context, true)}, child: Text(getS().confirm)), ], )); } ///是否有存储权限 Future hasStoragePermission() async { if (Platform.isAndroid) { AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; //小于32用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限 if (info.version.sdkInt <= 32) { return await Permission.storage.isGranted; } //33及以上使用READ_MEDIA_IMAGES和READ_MEDIA_VIDEO权限 return await Permission.photos.isGranted && await Permission.videos.isGranted; } else if (Platform.isIOS) { return await Permission.photos.isGranted; } return true; } ///请求存储权限 Future requestStoreagePermission() async { if (await hasStoragePermission()) { return true; } if (await SpUtil.hasStoragePermissionRequested()) { return false; } bool bo = false; if (Platform.isAndroid) { AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt <= 32) { bo = await Permission.storage.request().isGranted; } else { Map result = await [ Permission.photos, Permission.videos, ].request(); bo = result[Permission.photos]!.isGranted && result[Permission.videos]!.isGranted; } } else if (Platform.isIOS) { bo = await Permission.photos.request().isGranted; } await SpUtil.setStoragePermissionRequested(true); return bo; } ///是否有定位权限 Future hasLocationPremission() { return Permission.locationWhenInUse.isGranted; } ///开启屏幕旋转 void screenEnableRotate() { SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, DeviceOrientation.portraitUp, DeviceOrientation.portraitDown ]); } ///关闭屏幕旋转 void screenDisableRotate() { SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); } ///牙齿区域左上 const String toothAreaLeftTop = "lt"; ///牙齿区域左下 const String toothAreaLeftBottom = "lb"; ///牙齿区域右上 const String toothAreaRightTop = "rt"; ///牙齿区域右下 const String toothAreaRightBottom = "rb"; ///牙齿区域翻译 String toothAreaTranslate(String area) { switch (area) { case toothAreaLeftTop: return getS().leftTopArea; case toothAreaLeftBottom: return getS().leftBototmArea; case toothAreaRightTop: return getS().rightTopArea; case toothAreaRightBottom: return getS().rightBottomArea; default: return getS().unselected; } } String makeFilePrefix( {required String name, required String idCard, required String mobile, required String area, required String wifi, required int time, required String userId}) { FilePrefixInfo info = FilePrefixInfo( name: name, idCard: idCard, mobile: mobile, time: time, area: area, wifi: wifi, userId: userId); String encode = info.encode(); if (encode.isEmpty) { return ""; } return "${encode}_"; } ///解码base64 String decodeBase64(String encode) { Uint8List list = base64.decode(encode); return String.fromCharCodes(list); } ///编码base64 String encodeBase64(String str) { return base64.encode(utf8.encode(str)); } ///验证身份证 bool validIdCard(String str) { //18位 return RegExp( r'^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$') .hasMatch(str) || //15位 RegExp(r'[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}$') .hasMatch(str); } ///验证手机号 bool validMobile(String str) { return RegExp(r'1[3-9]\d{9}$').hasMatch(str); } ///设置全屏 void setFullScreen(bool isFullScreen) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: isFullScreen ? [] : [SystemUiOverlay.top]); } ///读取WIFI名称 Future getWifiName() async { //是否有权限 bool hasLocationPermission = await hasLocationPremission(); if (!hasLocationPermission) { if (await SpUtil.hasLocationPermissionRequested()) { logd("读取wifi名称没有位置权限且已经请求过位置权限"); return null; } if (navigatorKey.currentState!.mounted) { logd("弹窗提示要请求位置权限"); await showDialog( context: navigatorKey.currentState!.context, builder: (ctx) { return AlertDialog( title: Text(getS().hint), content: Text(getS().requestLocationHint), actions: [ TextButton( onPressed: () => Navigator.of(navigatorKey.currentState!.context).pop(), child: Text(getS().confirm)) ], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15.r)), ); }); logd("请求位置权限"); PermissionStatus status = await Permission.locationWhenInUse.request(); await SpUtil.setLocationPermissionRequested(true); hasLocationPermission = status == PermissionStatus.granted; } } if (!hasLocationPermission) { logd("读取wifi名称没有位置权限"); return null; } NetworkInfo networkInfo = NetworkInfo(); String? name = await networkInfo.getWifiName(); if (name == null) { logd("读取wifi名称返回null"); return null; } logd("读取wifi名称=$name"); //android返回的wifi名称里带双引号 if (name.contains('"')) { name = name.replaceAll(RegExp(r'"'), ""); } return name; } ///获取设备型号,根据wifi名字获取,如果wifi名字为空就返回null Future getDeviceModel() async { String? wifiName = await getWifiName(); if (wifiName.isNullOrEmpty) { return null; } return wifiName!.replaceAll(deviceWifiPrefix, ""); } ///是否是设备WIFI,用来判断是否连接了设备 Future isDeviceWifi() async { String? name = await getWifiName(); //没有读取到wifi名称就默认是外网 if (name.isNullOrEmpty) { return false; } return SynchronousFuture(name!.startsWith(deviceWifiPrefix)); } ///检查是否外网WIFI /// /// [isShowToast] 是否弹提示 Future checkInternetWifi([bool isShowToast = true]) async { if (await isDeviceWifi()) { if (isShowToast) { showToast(text: getS().pleaseConnectInternetWifi); } return false; } return true; } ///检查是否已登录 /// /// [toLogin] 是否跳转登录页面 /// [cancelable] 登录页面是否可以取消,如果可以取消,则只push登录页面,否则将replace掉所有其他页面 bool checkLogin(BuildContext context, {bool toLogin = true, bool cancelable = true}) { if (hasToken) { return true; } if (toLogin && context.mounted) { if (cancelable) { context.pushRoute(LoginRoute(cancelable: cancelable)); } else { popAllRoutes(context); context.pushRoute(LoginRoute(cancelable: false)); } } return false; } ///前往用户协议 void gotoUserAgreement(BuildContext context, {bool checkWifi = true}) async { if (checkWifi) { if (!await checkInternetWifi()) { return; } } if (!context.mounted) { return; } context.pushRoute(WebviewRoute( url: "", title: getS().userAgreement, htmlFuture: ApiService(Http.instance.dio).getUserAgreement())); } ///前往隐私协议 void gotoPrivacyPolicy(BuildContext context, {bool checkWifi = true}) async { if (checkWifi) { if (!await checkInternetWifi()) { return; } } if (!context.mounted) { return; } context.pushRoute(WebviewRoute( url: "", title: getS().privacyPolicy, htmlFuture: ApiService(Http.instance.dio).getPrivacyPolicy())); } ///前往权限说明 void gotoPermissionDesc(BuildContext context, {bool checkWifi = true}) async { if (checkWifi) { if (!await checkInternetWifi()) { return; } } if (!context.mounted) { return; } context.pushRoute(WebviewRoute( url: "", title: getS().permissionDescription, htmlFuture: ApiService(Http.instance.dio) .getPermissionDescription(Platform.isIOS ? "ios" : "android"))); } ///给身份证加星号 String setIdCardStar(String idCard) { if (idCard.length <= 6) { return idCard; } return "${idCard.substring(0, 3).padRight(idCard.length - 3, "*")}${idCard.substring(idCard.length - 3)}"; } ///设置已选择的咨询人ID Future setSelectedPatientId(int id) async { bool bo = await SpUtil.setSelectedPatientId(id); if (bo) { selectedPatientId = id; } return bo; } ///从身份证中获取性别,0男性,1女性,-1错误 int getGenderFromIdCard(String idCard) { if (!(idCard.length == 15 || idCard.length == 18)) { return -1; } try { //15位身份证最后3位奇数男性,偶数女性 //18位身份证第17位奇数男性,偶数女性 int sex = int.parse( idCard.length == 15 ? idCard.substring(12) : idCard.substring(16, 17)); return sex.isOdd ? 0 : 1; } catch (e) { loge("从身份证中解析性别异常", error: e); } return -1; } ///从身份证中获取年龄,-1错误 int getAgeFromIdCard(String idCard) { if (!(idCard.length == 15 || idCard.length == 18)) { return -1; } try { int year, month, day; //18位身份证 if (idCard.length == 18) { year = int.parse(idCard.substring(6, 10)); month = int.parse(idCard.substring(10, 12)); day = int.parse(idCard.substring(12, 14)); } //15位身份证 else { //6,7位表示出生年份后两位 String str = idCard.substring(6, 8); int a = int.parse(str); int nowYear = DateTime.now().year; int thousandYear = int.parse("${"$nowYear".substring(0, 2)}00"); //如果身份证年份后两位比现在大,就把现在的千年减去100再加上后两位 if (a > nowYear - thousandYear) { year = thousandYear - 100 + a; } //否则就用现在的千年加上后两位 else { year = thousandYear + a; } month = int.parse(idCard.substring(8, 10)); day = int.parse(idCard.substring(10, 12)); } //生日 DateTime birth = DateTime(year, month, day); //年龄 int age = (DateTime.now().difference(birth).inDays / 365.0).floor(); //logd("从身份证中获取年龄,year=$year,month=$month,day=$day,age=$age"); return age; } catch (e) { loge("从身份证中获取年龄异常", error: e); } return -1; } final _key = encrpyt.Key.fromUtf8("1234567890123456"); final _iv = encrpyt.IV.fromUtf8("1234567890123456"); final _encrypter = encrpyt.Encrypter( encrpyt.AES(_key, mode: encrpyt.AESMode.cbc, padding: "PKCS7")); ///AES加密,返回base64 String aesEncrypt(String str) { if (str.isEmpty) { return ""; } var encrypted = _encrypter.encrypt(str, iv: _iv); return encrypted.base64; } ///AES解密,使用base64 String aesDecrypt(String str) { if (str.isEmpty) { return ""; } var encrptyed = encrpyt.Encrypted.from64(str); return _encrypter.decrypt(encrptyed, iv: _iv); } ///合并字节数组 Uint8List concatenateBytes(List byteList) { int totalLength = byteList.fold(0, (sum, bytes) => sum + bytes.length); Uint8List result = Uint8List(totalLength); int offset = 0; for (var bytes in byteList) { result.setRange(offset, offset + bytes.length, bytes); offset += bytes.length; } return result; } ///RSA加密,返回BASE64字符串,仅支持PKCS1编码 /// /// [keyBitLength] 密钥长度,应为512,1024,2048,4096中的一个,默认512 String rsaEncrypt(String pubKey, String content, {int keyBitLength = 512}) { try { final cipher = PKCS1Encoding(RSAEngine()) ..init( true, PublicKeyParameter( encrpyt.RSAKeyParser().parse(pubKey) as RSAPublicKey)); final utf8Bytes = Uint8List.fromList(utf8.encode(content)); final inputLen = utf8Bytes.length; int offLen = 0; int i = 0; List bops = []; int size = (keyBitLength / 8 - 11).toInt(); while (inputLen - offLen > 0) { final chunkSize = (inputLen - offLen > size) ? size : inputLen - offLen; final cache = cipher.process( Uint8List.sublistView(utf8Bytes, offLen, offLen + chunkSize)); bops.add(cache); i++; offLen = size * i; } final encryptedData = concatenateBytes(bops); final base64Encoded = base64Encode(encryptedData); return base64Encoded; } catch (e) { loge("RSA加密异常", error: e); return ""; } } ///RSA解密,输入BASE64,仅支持PKCS1编码 /// /// [keyBitLength] 密钥长度,应为512,1024,2048,4096中的一个,默认512 String rsaDecrypt(String priKey, String content, {int keyBitLength = 512}) { try { final cipher = PKCS1Encoding(RSAEngine()); cipher.init( false, PrivateKeyParameter( encrpyt.RSAKeyParser().parse(priKey) as RSAPrivateKey)); final encryptedBytes = base64Decode(content); final inputLen = encryptedBytes.length; int offLen = 0; int i = 0; List byteList = []; int size = keyBitLength ~/ 8; while (inputLen - offLen > 0) { final int chunkSize = (inputLen - offLen > size) ? size : inputLen - offLen; final cache = cipher.process( Uint8List.sublistView(encryptedBytes, offLen, offLen + chunkSize)); byteList.add(cache); i++; offLen = size * i; } final byteArray = concatenateBytes(byteList); return utf8.decode(byteArray); } catch (e) { loge("RSA解密异常", error: e); return ''; } } ///检查新版本 /// /// 返回true表示有新版本,false没有新版本 Future checkNewVersion(BuildContext context, WidgetRef ref) async { VersionInfo? info = await ref .read(checkNewVersionProvider.notifier) .checkNewVersion(Platform.isIOS ? "ios" : "android"); if (info == null || info.code.isNullOrEmpty) { logd("检查更新,服务器返回信息为null,或code为空"); return false; } PackageInfo packageInfo = await PackageInfo.fromPlatform(); logd( "检查更新,当前version=${packageInfo.version},当前code=${packageInfo.buildNumber},服务器version=${info.version},服务器code=${info.code}"); int versionCode = 0; int serverCode = 0; try { versionCode = int.parse(packageInfo.buildNumber); serverCode = int.parse(info.code ?? "0"); } catch (e) { loge("检查更新转换版本号异常", error: e); } //ios的version和code都需要比较,因为对于同一版本,ios发布时会自动提升code //不同版本又会从pubspec.yaml里填写的code重新计算 if (Platform.isIOS) { if (info.version.isNullOrEmpty) { logd("ios比较版本,服务器version为空"); return false; } //把服务器的版本号转为数字数组 List serverVersionNums = info.version!.split(".").map((e) { try { return int.parse(e); } catch (e) { loge("转换服务器版本字符到异常", error: e); } return 0; }).toList(); //把本地的版本号转为数字数组 List currentVersionNums = packageInfo.version.split(".").map((e) { try { return int.parse(e); } catch (e) { loge("转换本地版本字符到异常", error: e); } return 0; }).toList(); //如果本地长度小于服务器长度,则补齐本地数组 if (currentVersionNums.length < serverVersionNums.length) { currentVersionNums.addAll(List.filled( serverVersionNums.length - currentVersionNums.length, -1)); } //从左到右比较数字大小,只要服务器有一个大于本地的就认为需要升级 bool needUpdate = false; bool sameVersion = true; for (int i = 0; i < serverVersionNums.length; i++) { if (serverVersionNums[i] != currentVersionNums[i]) { sameVersion = false; } if (serverVersionNums[i] > currentVersionNums[i]) { needUpdate = true; break; } } //如果版本号都一致,就判断code的大小 if (!needUpdate && sameVersion && serverCode > versionCode) { needUpdate = true; } return needUpdate; } //其他的只需要判断code else { if (versionCode >= serverCode) { return false; } } if (context.mounted) { showDialog( context: context, builder: (ctx) { return AppUpdateDialog( version: info.version!, content: info.content ?? "", url: info.downloadUrl ?? "", isForce: info.mandatoryUpdate == "1"); }); } return true; } ///登出清空数据 Future logoutClearData() async { updateToken(""); await SpUtil.setUserId(""); await SpUtil.setUserName(""); await SpUtil.setToken(""); await setSelectedPatientId(-1); } ///弹出所有路由 void popAllRoutes(BuildContext context) { AutoRouter.of(context).popUntil((_) => false); } ///是否是慧视健康 bool get isHsjk => appFlavor == flavorHsjk; ///是否是慧视通 bool get isHst => appFlavor == flavorHst; ///app名字 String get appName => switch (appFlavor) { flavorHst => getS().appNameHst, _ => getS().appName }; ///退出APP void exitApp() async { await SystemNavigator.pop(); //exit(0); } ///前往国康在线问诊 void gotoGkOnlineConsultation(BuildContext context) async { if (!await checkInternetWifi(true)) { logd("前往国康在线问诊,没连接外网"); return; } if (!context.mounted) { return; } if (!checkLogin(context)) { logd("前往国康在线问诊,未登录"); return; } String baseUrl = kDebugMode ? urlGkOnlineConsultationBaseUrlTest : urlGkOnlineConsultationBaseUrl; Map map = { "uid": await SpUtil.getUserId(), "accessTime": DateTime.now().yyyyMMddHHmmss }; String rsa = rsaEncrypt(gkOnlineConsultationRsaPublicKey, jsonEncode(map)); String url = "$baseUrl?appId=$gkOnlineConsultationAppId&data=$rsa"; logd("前往国康在线问诊,data=$map,url=$url"); if (!context.mounted) { return; } context.pushRoute(OnlineConsultationRoute(url: url, backAlert: true)); }