diff --git a/README.md b/README.md index 57ebc34..5e52fee 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,34 @@ JS 全局对象: 所有方法都使用 Cordova callback 风格,适合在 OutSystems client action 中封装。 +```js +BluememeUtils.getDeviceInfo(function (info) { + console.log(info.deviceKey); +}, error); +``` + +`getDeviceInfo` 返回: + +```json +{ + "deviceKey": "generated-and-persisted-uuid", + "platform": "Android", + "appIdentifier": "com.company.app", + "appVersion": "1.0.0", + "buildNumber": "1", + "osVersion": "15", + "sdkVersion": 35, + "deviceModel": "Google Pixel 8", + "manufacturer": "Google", + "brand": "google", + "model": "Pixel 8", + "locale": "ja-JP", + "timeZone": "Asia/Tokyo" +} +``` + +`deviceKey` 首次调用时生成并保存在本地,用于标识当前 App 安装实例。App 卸载或清除数据后会重新生成。iOS 不返回 `sdkVersion`、`manufacturer`、`brand`,但会返回 `systemName`。 + ```js BluememeUtils.requestWifiPermission(success, error); diff --git a/src/android/BluememeUtils.java b/src/android/BluememeUtils.java index a8c469b..aeb917d 100644 --- a/src/android/BluememeUtils.java +++ b/src/android/BluememeUtils.java @@ -11,6 +11,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; @@ -47,11 +48,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.TimeZone; +import java.util.UUID; import java.util.concurrent.Executor; public class BluememeUtils extends CordovaPlugin { private static final String TAG = "BluememeUtils"; private static final String PREFS = "bluememe_utils"; + private static final String KEY_DEVICE_KEY = "device_key"; private static final String KEY_FCM_TOKEN = "fcm_token"; private static final String KEY_INITIAL_NOTIFICATION = "initial_notification"; private static final String ACTION_SUFFIX = ".BLUEMEME_UTILS_EVENT"; @@ -75,6 +79,10 @@ public class BluememeUtils extends CordovaPlugin { @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + if ("getDeviceInfo".equals(action)) { + getDeviceInfo(callbackContext); + return true; + } if ("saveWifi".equals(action)) { saveWifi(args.optJSONObject(0), callbackContext); return true; @@ -184,6 +192,32 @@ public class BluememeUtils extends CordovaPlugin { } } + private void getDeviceInfo(CallbackContext callbackContext) { + Context context = cordova.getActivity().getApplicationContext(); + try { + JSONObject result = new JSONObject(); + PackageInfo packageInfo = packageInfo(context); + + result.put("deviceKey", getOrCreateDeviceKey(context)); + result.put("platform", "Android"); + result.put("appIdentifier", context.getPackageName()); + result.put("appVersion", packageInfo == null || packageInfo.versionName == null ? "" : packageInfo.versionName); + result.put("buildNumber", packageInfo == null ? "" : String.valueOf(packageVersionCode(packageInfo))); + result.put("osVersion", Build.VERSION.RELEASE == null ? "" : Build.VERSION.RELEASE); + result.put("sdkVersion", Build.VERSION.SDK_INT); + result.put("deviceModel", deviceModel()); + result.put("manufacturer", Build.MANUFACTURER == null ? "" : Build.MANUFACTURER); + result.put("brand", Build.BRAND == null ? "" : Build.BRAND); + result.put("model", Build.MODEL == null ? "" : Build.MODEL); + result.put("locale", currentLocaleTag()); + result.put("timeZone", TimeZone.getDefault().getID()); + + callbackContext.success(result); + } catch (Exception ex) { + sendError(callbackContext, "DEVICE_INFO_FAILED", ex.getMessage()); + } + } + private void connectWifi(JSONObject options, CallbackContext callbackContext) { JSONObject normalized = normalizeWifiOptions(options); String validationError = validateWifiOptions(normalized); @@ -1124,6 +1158,60 @@ public class BluememeUtils extends CordovaPlugin { return value == null ? "" : String.valueOf(value); } + private static PackageInfo packageInfo(Context context) { + try { + PackageManager packageManager = context.getPackageManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return packageManager.getPackageInfo(context.getPackageName(), PackageManager.PackageInfoFlags.of(0)); + } + return packageManager.getPackageInfo(context.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException ex) { + Log.w(TAG, "Unable to read package info.", ex); + return null; + } + } + + private static long packageVersionCode(PackageInfo packageInfo) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return packageInfo.getLongVersionCode(); + } + return packageInfo.versionCode; + } + + private static String getOrCreateDeviceKey(Context context) { + SharedPreferences preferences = prefs(context); + String existing = preferences.getString(KEY_DEVICE_KEY, ""); + if (!TextUtils.isEmpty(existing)) { + return existing; + } + String generated = UUID.randomUUID().toString(); + preferences.edit().putString(KEY_DEVICE_KEY, generated).apply(); + return generated; + } + + private static String deviceModel() { + String manufacturer = Build.MANUFACTURER == null ? "" : Build.MANUFACTURER; + String model = Build.MODEL == null ? "" : Build.MODEL; + if (TextUtils.isEmpty(manufacturer)) { + return model; + } + if (TextUtils.isEmpty(model)) { + return manufacturer; + } + if (model.toLowerCase(Locale.US).startsWith(manufacturer.toLowerCase(Locale.US))) { + return model; + } + return manufacturer + " " + model; + } + + private static String currentLocaleTag() { + Locale locale = Locale.getDefault(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return locale.toLanguageTag(); + } + return locale.toString(); + } + static String eventAction(Context context) { return context.getPackageName() + ACTION_SUFFIX; } diff --git a/src/ios/CDVBluememeUtils.m b/src/ios/CDVBluememeUtils.m index b1b2401..1915845 100644 --- a/src/ios/CDVBluememeUtils.m +++ b/src/ios/CDVBluememeUtils.m @@ -5,6 +5,8 @@ #import #import +static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key"; + @interface CDVBluememeUtils () @property (nonatomic, copy) NSString *notificationCallbackId; @property (nonatomic, strong) NSDictionary *initialNotification; @@ -36,6 +38,33 @@ [self applyWifiCommand:command defaultJoinOnce:YES]; } +- (void)getDeviceInfo:(CDVInvokedUrlCommand *)command +{ + NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary]; + NSString *bundleIdentifier = [self safeString:[[NSBundle mainBundle] bundleIdentifier]]; + NSString *appVersion = [self safeString:[infoDictionary objectForKey:@"CFBundleShortVersionString"]]; + NSString *buildNumber = [self safeString:[infoDictionary objectForKey:@"CFBundleVersion"]]; + UIDevice *device = [UIDevice currentDevice]; + NSLocale *locale = [NSLocale currentLocale]; + NSString *localeIdentifier = [self safeString:[locale localeIdentifier]]; + NSString *timeZone = [self safeString:[[NSTimeZone localTimeZone] name]]; + + NSDictionary *result = @{ + @"deviceKey": [self getOrCreateDeviceKey], + @"platform": @"iOS", + @"appIdentifier": bundleIdentifier, + @"appVersion": appVersion, + @"buildNumber": buildNumber, + @"osVersion": [self safeString:device.systemVersion], + @"systemName": [self safeString:device.systemName], + @"deviceModel": [self safeString:device.model], + @"model": [self safeString:device.model], + @"locale": localeIdentifier, + @"timeZone": timeZone + }; + [self sendSuccessForCommand:command dictionary:result]; +} + - (void)removeWifi:(CDVInvokedUrlCommand *)command { NSDictionary *options = [self firstDictionaryArgument:command]; @@ -446,6 +475,20 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return @""; } +- (NSString *)safeString:(id)value +{ + if ([value isKindOfClass:[NSString class]]) { + return value; + } + if ([value respondsToSelector:@selector(stringValue)]) { + return [value stringValue]; + } + if ([value respondsToSelector:@selector(description)]) { + return [value description]; + } + return @""; +} + - (BOOL)boolFromDictionary:(NSDictionary *)dictionary key:(NSString *)key defaultValue:(BOOL)defaultValue { id value = dictionary[key]; @@ -473,6 +516,20 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response return @""; } +- (NSString *)getOrCreateDeviceKey +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *existing = [defaults stringForKey:BluememeDeviceKeyDefaultsKey]; + if (existing.length > 0) { + return existing; + } + + NSString *generated = [[NSUUID UUID] UUIDString]; + [defaults setObject:generated forKey:BluememeDeviceKeyDefaultsKey]; + [defaults synchronize]; + return generated; +} + - (void)sendSuccessForCommand:(CDVInvokedUrlCommand *)command dictionary:(NSDictionary *)dictionary { CDVPluginResult *result = dictionary == nil diff --git a/www/BluememeUtils.js b/www/BluememeUtils.js index 31237b5..5472296 100644 --- a/www/BluememeUtils.js +++ b/www/BluememeUtils.js @@ -45,6 +45,10 @@ function dispatchLocalEvent(event) { } var api = { + getDeviceInfo: function (success, error) { + call('getDeviceInfo', [], success, error); + }, + saveWifi: function (options, success, error) { call('saveWifi', [asWifiOptions(options)], success, error); },