diff --git a/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs b/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs new file mode 100644 index 0000000..eeb809f --- /dev/null +++ b/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs @@ -0,0 +1,90 @@ +#if UNITY_IOS +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; +using System.IO; +using System.Text; +using UnityEngine; + +/// +/// iOS 权限描述双语本地化: +/// - Info.plist 写入英文描述作为默认(所有非中文用户看到英文) +/// - 生成 zh-Hans.lproj/InfoPlist.strings,中文系统用户自动看到中文 +/// 最高优先级执行,确保覆盖其他插件写入的权限描述。 +/// +public class iOSLocalizedPermissionPostProcessor +{ + // 权限描述中英文对照 + private static readonly (string key, string en, string zhHans)[] PERMISSIONS = + { + ("NSCameraUsageDescription", + "Camera access is used to scan the QR code on your device for pairing. For example, scan the QR code on the device body to complete the connection.", + "需要相机权限来扫描设备上的二维码完成配对。例如:扫描设备机身上的二维码后完成连接。"), + ("NSLocationWhenInUseUsageDescription", + "Location access is used to detect the current WiFi network so we can display the connected WiFi name.", + "需要位置权限来获取当前 WiFi 信息,用于展示已连接的 WiFi 名称。"), + ("NSBluetoothAlwaysUsageDescription", + "Bluetooth is used to scan for and connect to your device. For example, after pairing with the device, we send control commands to it.", + "需要蓝牙权限来搜索并连接你的设备。例如:设备配对成功后向其发送控制指令。"), + ("NSBluetoothPeripheralUsageDescription", + "Bluetooth is used to scan for and connect to your device. For example, after pairing with the device, we send control commands to it.", + "需要蓝牙权限来搜索并连接你的设备。例如:设备配对成功后向其发送控制指令。"), + ("NSPhotoLibraryUsageDescription", + "Allows you to choose photos from your library to set as your profile picture or attach to feedback reports. For example, select a photo when you tap your avatar in the profile page.", + "允许你从相册选择照片设置头像或上传问题反馈截图。例如:在个人主页点击头像时选择照片进行更换。"), + ("NSPhotoLibraryAddUsageDescription", + "Allows saving generated QR code images and downloaded videos to your photo library. For example, save a QR code to your library so you can share it with friends.", + "允许将生成的二维码图片和下载的视频保存到你的相册。例如:将二维码保存到相册后分享给好友。"), + }; + + [PostProcessBuild(2000)] + public static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject) + { + if (target != BuildTarget.iOS) return; + + // 1. Info.plist 写入英文描述(默认回退语言) + string plistPath = Path.Combine(pathToBuiltProject, "Info.plist"); + PlistDocument plist = new PlistDocument(); + plist.ReadFromString(File.ReadAllText(plistPath)); + foreach (var p in PERMISSIONS) + { + plist.root.SetString(p.key, p.en); + } + File.WriteAllText(plistPath, plist.WriteToString()); + + // 2. 生成中文本地化文件 InfoPlist.strings + WriteChineseStringsFile(pathToBuiltProject); + + Debug.Log("[iOSLocalizedPermissionPostProcessor] 权限描述已本地化 (默认英文 / 中文自动切换)"); + } + + private static void WriteChineseStringsFile(string buildPath) + { + string lprojDir = Path.Combine(buildPath, "zh-Hans.lproj"); + Directory.CreateDirectory(lprojDir); + + var sb = new StringBuilder(); + foreach (var p in PERMISSIONS) + { + sb.AppendLine($"\"{p.key}\" = \"{p.zhHans}\";"); + } + + // 使用带 BOM 的 UTF-8 编码,兼容 Xcode + string filePath = Path.Combine(lprojDir, "InfoPlist.strings"); + File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(true)); + + // 3. 将 InfoPlist.strings 添加到 Xcode 工程的 Resources 构建阶段 + string pbxPath = PBXProject.GetPBXProjectPath(buildPath); + PBXProject proj = new PBXProject(); + proj.ReadFromString(File.ReadAllText(pbxPath)); + + string targetGuid = proj.GetUnityMainTargetGuid(); + string fileGuid = proj.AddFile(filePath, Path.Combine("zh-Hans.lproj", "InfoPlist.strings"), PBXSourceTree.Source); + proj.AddFileToBuild(targetGuid, fileGuid); + + File.WriteAllText(pbxPath, proj.WriteToString()); + + Debug.Log($"[iOSLocalizedPermissionPostProcessor] 已添加中文本地化: {filePath}"); + } +} +#endif diff --git a/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs.meta b/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs.meta new file mode 100644 index 0000000..e527ac6 --- /dev/null +++ b/Assets/Editor/iOSLocalizedPermissionPostProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ebd0bc243972540f0bd8a732dc3ec572 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/iOSPermissionPostProcessor.cs b/Assets/Editor/iOSPermissionPostProcessor.cs index c974e8a..7750052 100644 --- a/Assets/Editor/iOSPermissionPostProcessor.cs +++ b/Assets/Editor/iOSPermissionPostProcessor.cs @@ -18,7 +18,7 @@ public class iOSPermissionPostProcessor PlistElementDict rootDict = plist.root; // 添加摄像头权限描述 - rootDict.SetString("NSCameraUsageDescription", "需要摄像头权限来扫描二维码连接设备"); + rootDict.SetString("NSCameraUsageDescription", "Camera access is used to scan the QR code on your device for pairing. For example, scan the QR code on the device body to complete the connection."); File.WriteAllText(plistPath, plist.WriteToString()); } diff --git a/Assets/Editor/iOSWiFiBuildPostProcessor.cs b/Assets/Editor/iOSWiFiBuildPostProcessor.cs index e2fde88..f33d78a 100644 --- a/Assets/Editor/iOSWiFiBuildPostProcessor.cs +++ b/Assets/Editor/iOSWiFiBuildPostProcessor.cs @@ -8,7 +8,8 @@ public class iOSWiFiBuildPostProcessor { private const string WIFI_INFO_ENTITLEMENT = "com.apple.developer.networking.wifi-info"; - [PostProcessBuild] + // 高于默认顺序执行,确保覆盖其他插件写入的蓝牙描述 + [PostProcessBuild(1000)] public static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject) { if (target != BuildTarget.iOS) return; @@ -21,11 +22,11 @@ public class iOSWiFiBuildPostProcessor PlistElementDict rootDict = plist.root; // 添加位置权限描述(iOS 13+ 获取 WiFi 需要) - rootDict.SetString("NSLocationWhenInUseUsageDescription", "需要位置权限来获取当前 WiFi 信息"); + rootDict.SetString("NSLocationWhenInUseUsageDescription", "Location access is used to detect the current WiFi network so we can display the connected WiFi name."); // 添加蓝牙权限描述 - rootDict.SetString("NSBluetoothAlwaysUsageDescription", "需要蓝牙权限来连接设备"); - rootDict.SetString("NSBluetoothPeripheralUsageDescription", "需要蓝牙权限来连接设备"); + rootDict.SetString("NSBluetoothAlwaysUsageDescription", "Bluetooth is used to scan for and connect to your device. For example, after pairing with the device, we send control commands to it."); + rootDict.SetString("NSBluetoothPeripheralUsageDescription", "Bluetooth is used to scan for and connect to your device. For example, after pairing with the device, we send control commands to it."); // 添加后台模式 PlistElementArray bgModes = rootDict.CreateArray("UIBackgroundModes"); diff --git a/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs b/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs index ea33725..7b989f1 100644 --- a/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs +++ b/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs @@ -14,8 +14,8 @@ namespace NativeGalleryNamespace private const string SAVE_PATH = "ProjectSettings/NativeGallery.json"; public bool AutomatedSetup = true; - public string PhotoLibraryUsageDescription = "The app requires access to Photos to interact with it."; - public string PhotoLibraryAdditionsUsageDescription = "The app requires access to Photos to save media to it."; + public string PhotoLibraryUsageDescription = "Allows you to choose photos from your library to set as your profile picture or attach to feedback reports. For example, select a photo when you tap your avatar in the profile page."; + public string PhotoLibraryAdditionsUsageDescription = "Allows saving generated QR code images and downloaded videos to your photo library. For example, save a QR code to your library so you can share it with friends."; public bool DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = true; // See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/ private static Settings m_instance = null; diff --git a/Assets/Plugins/iOS/WiFiHelper.mm b/Assets/Plugins/iOS/WiFiHelper.mm index fb32c82..90a974a 100644 --- a/Assets/Plugins/iOS/WiFiHelper.mm +++ b/Assets/Plugins/iOS/WiFiHelper.mm @@ -2,10 +2,119 @@ #import #import #import +#import static CLLocationManager *locationManager = nil; +// UnitySendMessage 声明 +extern void UnitySendMessage(const char *obj, const char *method, const char *msg); + +// 授权变化回调目标(通过 UnitySendMessage 通知 C#) +static const char *callbackGameObject = nil; +static const char *callbackMethod = nil; + +// GetWiFiSSID 前向声明(定义在 extern "C" 块中) +extern "C" const char* GetWiFiSSID(); + +// 是否正在等待定位更新(收到第一个位置后立即获取 SSID) +static BOOL pendingSSIDFetch = NO; + +// 延迟获取 SSID 并回调 C# +static void FetchSSIDAndCallback() { + char *ssid = (char *)GetWiFiSSID(); + if (callbackGameObject != nil && callbackMethod != nil) { + if (ssid != NULL) { + UnitySendMessage(callbackGameObject, callbackMethod, ssid); + free(ssid); + } else { + UnitySendMessage(callbackGameObject, callbackMethod, ""); + } + } +} + +@interface WiFiHelperLocationDelegate : NSObject +@end + +@implementation WiFiHelperLocationDelegate +- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager { + if (@available(iOS 14.0, *)) { + // iOS 14+ 新回调:参数是 manager,需自行查询授权状态 + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + if (status == kCLAuthorizationStatusAuthorizedWhenInUse || + status == kCLAuthorizationStatusAuthorizedAlways) { + NSLog(@"[WiFiHelper] Location permission granted after request"); + [WiFiHelperLocationDelegate startSSIDFetchFlow]; + } else if (status == kCLAuthorizationStatusDenied || + status == kCLAuthorizationStatusRestricted) { + NSLog(@"[WiFiHelper] Location permission denied after request"); + if (callbackGameObject != nil && callbackMethod != nil) { + UnitySendMessage(callbackGameObject, callbackMethod, ""); + } + } + } +} +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + if (@available(iOS 14.0, *)) return; // iOS 14+ 由上面的新回调处理 + // iOS 13 及以下用旧回调方法 + if (status == kCLAuthorizationStatusAuthorizedWhenInUse || + status == kCLAuthorizationStatusAuthorizedAlways) { + NSLog(@"[WiFiHelper] Location permission granted after request (legacy callback)"); + [WiFiHelperLocationDelegate startSSIDFetchFlow]; + } else if (status == kCLAuthorizationStatusDenied || + status == kCLAuthorizationStatusRestricted) { + NSLog(@"[WiFiHelper] Location permission denied after request (legacy callback)"); + if (callbackGameObject != nil && callbackMethod != nil) { + UnitySendMessage(callbackGameObject, callbackMethod, ""); + } + } +} +// 收到第一个位置更新后再获取 SSID(NEHotspotNetwork.fetchCurrent 的隐式要求) +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + if (pendingSSIDFetch) { + NSLog(@"[WiFiHelper] First location update received, fetching SSID now"); + pendingSSIDFetch = NO; + [manager stopUpdatingLocation]; + FetchSSIDAndCallback(); + } +} +// 定位失败时也兜底尝试获取一次 +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { + NSLog(@"[WiFiHelper] Location manager failed: %@", error.localizedDescription); + if (pendingSSIDFetch) { + pendingSSIDFetch = NO; + [manager stopUpdatingLocation]; + FetchSSIDAndCallback(); + } +} + +// 启动定位并等待第一个位置更新,超时则直接获取 ++ (void)startSSIDFetchFlow { + if (![CLLocationManager locationServicesEnabled]) { + NSLog(@"[WiFiHelper] Location services disabled on device, fetching SSID anyway"); + FetchSSIDAndCallback(); + return; + } + if (locationManager == nil) { + locationManager = [[CLLocationManager alloc] init]; + } + pendingSSIDFetch = YES; + [locationManager startUpdatingLocation]; + // 超时兜底:定位迟迟无响应(如首次定位慢)时,3 秒后直接获取 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + if (pendingSSIDFetch) { + NSLog(@"[WiFiHelper] Timeout waiting for location update, fetching SSID anyway"); + pendingSSIDFetch = NO; + [locationManager stopUpdatingLocation]; + FetchSSIDAndCallback(); + } + }); +} +@end + +static WiFiHelperLocationDelegate *locationDelegate = nil; + extern "C" { + void RequestLocationPermission() { if (locationManager != nil) return; @@ -31,12 +140,52 @@ extern "C" { NSLog(@"[WiFiHelper] Location permission denied/restricted (status: %d)", (int)status); } } - - const char* GetWiFiSSID() { - // 确保位置已启动至少一次(NEHotspotNetwork 的隐式要求) - if (locationManager != nil && [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) { - [locationManager startUpdatingLocation]; + + /// + /// 请求位置权限(如未授权),授权完成后自动获取 WiFi 名称并回调 C#。 + /// 已授权时立即获取并回调。 + /// + void RequestWiFiSSIDNative(const char *gameObjectName, const char *methodName) { + if (callbackGameObject != nil) { + free((void *)callbackGameObject); } + if (callbackMethod != nil) { + free((void *)callbackMethod); + } + callbackGameObject = gameObjectName ? strdup(gameObjectName) : nil; + callbackMethod = methodName ? strdup(methodName) : nil; + + if (locationManager == nil) { + locationManager = [[CLLocationManager alloc] init]; + } + if (locationDelegate == nil) { + locationDelegate = [[WiFiHelperLocationDelegate alloc] init]; + } + locationManager.delegate = locationDelegate; + + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + if (status == kCLAuthorizationStatusAuthorizedWhenInUse || + status == kCLAuthorizationStatusAuthorizedAlways) { + NSLog(@"[WiFiHelper] Location already granted, fetching SSID directly"); + [WiFiHelperLocationDelegate startSSIDFetchFlow]; + return; + } + + if (status == kCLAuthorizationStatusNotDetermined) { + NSLog(@"[WiFiHelper] Requesting location permission, will fetch SSID on grant"); + [locationManager requestWhenInUseAuthorization]; + } else { + NSLog(@"[WiFiHelper] Location permission denied/restricted (status: %d)", (int)status); + if (callbackGameObject != nil && callbackMethod != nil) { + UnitySendMessage(callbackGameObject, callbackMethod, ""); + } + } + } + + const char* GetWiFiSSID() { + CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + BOOL locationEnabled = [CLLocationManager locationServicesEnabled]; + NSLog(@"[WiFiHelper] GetWiFiSSID: auth=%d servicesEnabled=%d", (int)status, (int)locationEnabled); __block NSString *resultSSID = nil; __block BOOL finished = NO; @@ -47,6 +196,8 @@ extern "C" { [NEHotspotNetwork fetchCurrentWithCompletionHandler:^(NEHotspotNetwork * _Nullable network) { if (network && network.SSID.length > 0) { resultSSID = network.SSID; + } else { + NSLog(@"[WiFiHelper] NEHotspotNetwork fetchCurrent returned nil"); } finished = YES; }]; @@ -79,10 +230,6 @@ extern "C" { } } - if (locationManager != nil) { - [locationManager stopUpdatingLocation]; - } - if (resultSSID == nil) { NSLog(@"[WiFiHelper] No WiFi SSID found."); return NULL; diff --git a/Assets/Scripts/UI/Pages/ConnectDevice/ConnectDevicePageCtrl.cs b/Assets/Scripts/UI/Pages/ConnectDevice/ConnectDevicePageCtrl.cs index 3593d21..0f3088c 100644 --- a/Assets/Scripts/UI/Pages/ConnectDevice/ConnectDevicePageCtrl.cs +++ b/Assets/Scripts/UI/Pages/ConnectDevice/ConnectDevicePageCtrl.cs @@ -154,9 +154,21 @@ public class ConnectDevicePageCtrl : MonoBehaviour g.gameObject.SetActive(false); sonPages[3].SetActive(true); +#if UNITY_IOS && !UNITY_EDITOR + // iOS:异步请求位置权限,授权后自动填入 WiFi 名称 + wifiNameInput.text = ""; + iOSWiFiHelper.RequestWiFiSSID((ssid) => + { + if (!string.IsNullOrEmpty(ssid)) + { + wifiNameInput.text = ssid; + } + }); +#else // 获取当前 WiFi 名称 string wifiName = GetCurrentWiFiName(); wifiNameInput.text = wifiName ?? ""; +#endif wifiPasswordInput.text=""; diff --git a/Assets/Scripts/UI/Pages/DeviceInfoPage/SetWifiPage.cs b/Assets/Scripts/UI/Pages/DeviceInfoPage/SetWifiPage.cs index 488c0c1..7288cb5 100644 --- a/Assets/Scripts/UI/Pages/DeviceInfoPage/SetWifiPage.cs +++ b/Assets/Scripts/UI/Pages/DeviceInfoPage/SetWifiPage.cs @@ -26,8 +26,21 @@ namespace Kill.UI.Pages backButton.onClick.AddListener(Back); confirmButton.onClick.RemoveAllListeners(); confirmButton.onClick.AddListener(OnWifiConfirmClick); +#if UNITY_IOS && !UNITY_EDITOR + // iOS:异步请求位置权限,授权后自动填入 WiFi 名称(无需重新打开页面) + ssidInput.text = ""; + iOSWiFiHelper.RequestWiFiSSID((ssid) => + { + if (!string.IsNullOrEmpty(ssid)) + { + ssidInput.text = ssid; + CheckWifiNameAndPassword(); + } + }); +#else string wifiName = GetCurrentWiFiName(); ssidInput.text = wifiName ?? ""; +#endif passwordInput.text = ""; CheckWifiNameAndPassword(); ssidInput.onEndEdit.RemoveAllListeners(); diff --git a/Assets/Scripts/iOSWiFiHelper.cs b/Assets/Scripts/iOSWiFiHelper.cs new file mode 100644 index 0000000..301352d --- /dev/null +++ b/Assets/Scripts/iOSWiFiHelper.cs @@ -0,0 +1,48 @@ +using System; +using System.Runtime.InteropServices; +using UnityEngine; + +/// +/// iOS WiFi 名称异步获取辅助类。 +/// 自动请求位置权限(iOS 14+ NEHotspotNetwork 需要), +/// 用户授权后回调返回当前连接的 WiFi 名称,无需重新打开页面。 +/// +public class iOSWiFiHelper : MonoBehaviour +{ + private static iOSWiFiHelper _instance; + private static Action _callback; + + /// + /// 请求位置权限并获取当前 WiFi 名称,授权后异步回调。 + /// + /// 回调参数为 WiFi SSID,获取失败或用户拒绝时为 null + public static void RequestWiFiSSID(Action callback) + { + _callback = callback; +#if UNITY_IOS && !UNITY_EDITOR + EnsureInstance(); + RequestWiFiSSIDNative(_instance.name, "OnWiFiSSIDResult"); +#endif + } + + private static void EnsureInstance() + { + if (_instance != null) return; + var go = new GameObject("iOSWiFiHelper"); + DontDestroyOnLoad(go); + _instance = go.AddComponent(); + } + + /// 原生层通过 UnitySendMessage 回调此方法 + public void OnWiFiSSIDResult(string ssid) + { + var cb = _callback; + _callback = null; + cb?.Invoke(string.IsNullOrEmpty(ssid) ? null : ssid); + } + +#if UNITY_IOS && !UNITY_EDITOR + [DllImport("__Internal")] + private static extern void RequestWiFiSSIDNative(string gameObjectName, string methodName); +#endif +} diff --git a/Assets/Scripts/iOSWiFiHelper.cs.meta b/Assets/Scripts/iOSWiFiHelper.cs.meta new file mode 100644 index 0000000..a5dfb56 --- /dev/null +++ b/Assets/Scripts/iOSWiFiHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12a95f347f56e4858946e8ed0fe12caa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 1c1fbce..fdadad2 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -145,7 +145,7 @@ PlayerSettings: loadStoreDebugModeEnabled: 0 visionOSBundleVersion: 1.0 tvOSBundleVersion: 1.0 - bundleVersion: 1.0.2 + bundleVersion: 1.0.3 preloadedAssets: - {fileID: 7756680987677051820, guid: 0488490a53b3eb2409be4727abdf829a, type: 2} - {fileID: 11400000, guid: d990b2e2bc28cca43863c18940a862a3, type: 2} @@ -174,7 +174,7 @@ PlayerSettings: buildNumber: Standalone: 0 VisionOS: 0 - iPhone: 3 + iPhone: 0 tvOS: 0 overrideDefaultApplicationIdentifier: 1 AndroidBundleVersionCode: 3 @@ -565,10 +565,10 @@ PlayerSettings: enableInternalProfiler: 0 logObjCUncaughtExceptions: 1 enableCrashReportAPI: 0 - cameraUsageDescription: "\u7528\u4E8E\u626B\u63CF\u4E8C\u7EF4\u7801" - locationUsageDescription: "\u7528\u4E8E\u84DD\u7259\u8BBE\u5907\u901A\u4FE1" + cameraUsageDescription: "Camera access is used to scan the QR code on your device for pairing." + locationUsageDescription: "Location access is used to detect the current WiFi network." microphoneUsageDescription: - bluetoothUsageDescription: + bluetoothUsageDescription: "Bluetooth is used to scan for and connect to your device." macOSTargetOSVersion: 10.13.0 switchNMETAOverride: switchNetLibKey: