package com.bluememe.utils; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.app.KeyguardManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.fingerprint.FingerprintManager; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkSpecifier; import android.net.wifi.WifiNetworkSuggestion; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.text.TextUtils; import android.util.Log; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.messaging.FirebaseMessaging; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.PermissionHelper; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; 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_FCM_TOKEN = "fcm_token"; private static final String KEY_INITIAL_NOTIFICATION = "initial_notification"; private static final String ACTION_SUFFIX = ".BLUEMEME_UTILS_EVENT"; private static final String EXTRA_EVENT = "event"; private static final String EXTRA_PAYLOAD = "bluememe_payload"; private static final int REQ_WIFI_PERMISSION = 8101; private static final int REQ_NOTIFICATION_PERMISSION = 8102; private CallbackContext wifiPermissionCallback; private CallbackContext notificationPermissionCallback; private CallbackContext notificationCallback; private ConnectivityManager.NetworkCallback networkCallback; private BroadcastReceiver eventReceiver; @Override protected void pluginInitialize() { ensureFirebaseInitialized(false); registerEventReceiver(); captureLaunchIntent(cordova.getActivity().getIntent()); } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { if ("saveWifi".equals(action)) { saveWifi(args.optJSONObject(0), callbackContext); return true; } if ("connectWifi".equals(action)) { connectWifi(args.optJSONObject(0), callbackContext); return true; } if ("disconnectWifi".equals(action)) { disconnectWifi(callbackContext); return true; } if ("removeWifi".equals(action)) { removeWifi(args.optJSONObject(0), callbackContext); return true; } if ("requestWifiPermission".equals(action)) { requestWifiPermission(callbackContext); return true; } if ("isBiometricAvailable".equals(action)) { isBiometricAvailable(callbackContext); return true; } if ("unlockWithBiometrics".equals(action)) { unlockWithBiometrics(args.optJSONObject(0), callbackContext); return true; } if ("requestPushPermission".equals(action)) { requestPushPermission(callbackContext); return true; } if ("getFirebaseToken".equals(action)) { getFirebaseToken(callbackContext); return true; } if ("subscribeToTopic".equals(action)) { subscribeToTopic(args.optString(0, ""), callbackContext); return true; } if ("unsubscribeFromTopic".equals(action)) { unsubscribeFromTopic(args.optString(0, ""), callbackContext); return true; } if ("getInitialNotification".equals(action)) { getInitialNotification(callbackContext); return true; } if ("startNotificationListener".equals(action)) { startNotificationListener(callbackContext); return true; } if ("stopNotificationListener".equals(action)) { notificationCallback = null; callbackContext.success(); return true; } return false; } @Override public void onNewIntent(Intent intent) { captureLaunchIntent(intent); } @Override public void onDestroy() { unregisterEventReceiver(); stopNetworkRequest(); super.onDestroy(); } @Override public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { if (requestCode == REQ_WIFI_PERMISSION) { CallbackContext callback = wifiPermissionCallback; wifiPermissionCallback = null; if (callback != null) { callback.success(permissionResult("wifi", allGranted(grantResults), missingWifiPermissions())); } return; } if (requestCode == REQ_NOTIFICATION_PERMISSION) { CallbackContext callback = notificationPermissionCallback; notificationPermissionCallback = null; if (callback != null) { callback.success(permissionResult("notification", allGranted(grantResults), new JSONArray())); } } } private void saveWifi(JSONObject options, CallbackContext callbackContext) { JSONObject normalized = normalizeWifiOptions(options); String validationError = validateWifiOptions(normalized); if (validationError != null) { sendError(callbackContext, "INVALID_WIFI_OPTIONS", validationError); return; } if (!hasWifiPermissions()) { sendError(callbackContext, "PERMISSION_REQUIRED", "Call requestWifiPermission before saving a WiFi network."); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { saveWifiSuggestion(normalized, callbackContext); } else { saveLegacyWifi(normalized, callbackContext); } } private void connectWifi(JSONObject options, CallbackContext callbackContext) { JSONObject normalized = normalizeWifiOptions(options); String validationError = validateWifiOptions(normalized); if (validationError != null) { sendError(callbackContext, "INVALID_WIFI_OPTIONS", validationError); return; } if (!hasWifiPermissions()) { sendError(callbackContext, "PERMISSION_REQUIRED", "Call requestWifiPermission before connecting to a WiFi network."); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { connectWithSpecifier(normalized, callbackContext); } else { connectLegacyWifi(normalized, callbackContext); } } private void removeWifi(JSONObject options, CallbackContext callbackContext) { JSONObject normalized = normalizeWifiOptions(options); String ssid = normalized.optString("ssid", ""); if (TextUtils.isEmpty(ssid)) { sendError(callbackContext, "INVALID_WIFI_OPTIONS", "ssid is required."); return; } if (!hasWifiPermissions()) { sendError(callbackContext, "PERMISSION_REQUIRED", "Call requestWifiPermission before removing a WiFi network."); return; } WifiManager wifiManager = wifiManager(); if (wifiManager == null) { sendError(callbackContext, "WIFI_UNAVAILABLE", "WiFi manager is not available."); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { try { int status = wifiManager.removeNetworkSuggestions(Collections.singletonList(buildSuggestion(normalized))); JSONObject result = new JSONObject(); result.put("ssid", ssid); result.put("platformMode", "suggestion"); result.put("status", status); result.put("removed", status == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS); callbackContext.success(result); } catch (Exception ex) { sendError(callbackContext, "WIFI_REMOVE_FAILED", ex.getMessage()); } return; } int networkId = findLegacyNetworkId(wifiManager, ssid); if (networkId < 0) { try { JSONObject result = new JSONObject(); result.put("ssid", ssid); result.put("removed", false); result.put("reason", "not_found"); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } return; } boolean removed = wifiManager.removeNetwork(networkId); wifiManager.saveConfiguration(); try { JSONObject result = new JSONObject(); result.put("ssid", ssid); result.put("networkId", networkId); result.put("removed", removed); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } private void disconnectWifi(CallbackContext callbackContext) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { stopNetworkRequest(); try { JSONObject result = new JSONObject(); result.put("disconnected", true); result.put("platformMode", "specifier"); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } return; } WifiManager wifiManager = wifiManager(); if (wifiManager == null) { sendError(callbackContext, "WIFI_UNAVAILABLE", "WiFi manager is not available."); return; } boolean disconnected = wifiManager.disconnect(); try { JSONObject result = new JSONObject(); result.put("disconnected", disconnected); result.put("platformMode", "legacy"); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } private void requestWifiPermission(CallbackContext callbackContext) { JSONArray missing = missingWifiPermissions(); if (missing.length() == 0) { try { callbackContext.success(permissionResult("wifi", true, missing)); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } return; } wifiPermissionCallback = callbackContext; PermissionHelper.requestPermissions(this, REQ_WIFI_PERMISSION, jsonArrayToStringArray(missing)); } private void isBiometricAvailable(CallbackContext callbackContext) { try { callbackContext.success(biometricAvailability()); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } private void unlockWithBiometrics(JSONObject options, CallbackContext callbackContext) { try { JSONObject availability = biometricAvailability(); if (!availability.optBoolean("available")) { sendError(callbackContext, availability.optString("code", "BIOMETRIC_UNAVAILABLE"), availability.optString("message", "Biometric authentication is unavailable.")); return; } } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); return; } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { sendError(callbackContext, "UNSUPPORTED_ANDROID_VERSION", "Biometric unlock requires Android 9.0 or newer in this plugin."); return; } final JSONObject safeOptions = options == null ? new JSONObject() : options; cordova.getActivity().runOnUiThread(new Runnable() { @Override public void run() { showBiometricPrompt(safeOptions, callbackContext); } }); } private void requestPushPermission(CallbackContext callbackContext) { if (Build.VERSION.SDK_INT < 33 || PermissionHelper.hasPermission(this, Manifest.permission.POST_NOTIFICATIONS)) { try { callbackContext.success(permissionResult("notification", true, new JSONArray())); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } return; } notificationPermissionCallback = callbackContext; PermissionHelper.requestPermission(this, REQ_NOTIFICATION_PERMISSION, Manifest.permission.POST_NOTIFICATIONS); } private void getFirebaseToken(final CallbackContext callbackContext) { if (!ensureFirebaseInitialized(true)) { sendError(callbackContext, "FIREBASE_NOT_CONFIGURED", "Firebase is not configured. Add Google services files or pass Firebase plugin preferences."); return; } FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(Task task) { if (!task.isSuccessful()) { Exception exception = task.getException(); sendError(callbackContext, "FCM_TOKEN_FAILED", exception == null ? "Failed to get Firebase token." : exception.getMessage()); return; } String token = task.getResult(); saveToken(cordova.getActivity(), token); try { JSONObject result = new JSONObject(); result.put("token", token); callbackContext.success(result); JSONObject event = new JSONObject(); event.put("type", "token"); event.put("token", token); sendEventToJs(event); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } }); } private void subscribeToTopic(String topic, final CallbackContext callbackContext) { if (TextUtils.isEmpty(topic)) { sendError(callbackContext, "INVALID_TOPIC", "topic is required."); return; } if (!ensureFirebaseInitialized(true)) { sendError(callbackContext, "FIREBASE_NOT_CONFIGURED", "Firebase is not configured."); return; } FirebaseMessaging.getInstance().subscribeToTopic(topic).addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(Task task) { completeFirebaseTask(task, callbackContext); } }); } private void unsubscribeFromTopic(String topic, final CallbackContext callbackContext) { if (TextUtils.isEmpty(topic)) { sendError(callbackContext, "INVALID_TOPIC", "topic is required."); return; } if (!ensureFirebaseInitialized(true)) { sendError(callbackContext, "FIREBASE_NOT_CONFIGURED", "Firebase is not configured."); return; } FirebaseMessaging.getInstance().unsubscribeFromTopic(topic).addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(Task task) { completeFirebaseTask(task, callbackContext); } }); } private void getInitialNotification(CallbackContext callbackContext) { SharedPreferences preferences = prefs(cordova.getActivity()); String value = preferences.getString(KEY_INITIAL_NOTIFICATION, null); preferences.edit().remove(KEY_INITIAL_NOTIFICATION).apply(); if (TextUtils.isEmpty(value)) { callbackContext.success((JSONObject) null); return; } try { callbackContext.success(new JSONObject(value)); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } private void startNotificationListener(CallbackContext callbackContext) { notificationCallback = callbackContext; PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callbackContext.sendPluginResult(result); String token = prefs(cordova.getActivity()).getString(KEY_FCM_TOKEN, null); if (!TextUtils.isEmpty(token)) { try { JSONObject event = new JSONObject(); event.put("type", "token"); event.put("token", token); sendEventToJs(event); } catch (JSONException ignored) { Log.w(TAG, "Failed to dispatch cached FCM token."); } } } @TargetApi(Build.VERSION_CODES.Q) private void saveWifiSuggestion(JSONObject options, CallbackContext callbackContext) { WifiManager wifiManager = wifiManager(); if (wifiManager == null) { sendError(callbackContext, "WIFI_UNAVAILABLE", "WiFi manager is not available."); return; } try { WifiNetworkSuggestion suggestion = buildSuggestion(options); int status = wifiManager.addNetworkSuggestions(Collections.singletonList(suggestion)); if (status != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS && status != WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE) { sendError(callbackContext, "WIFI_SAVE_FAILED", "addNetworkSuggestions returned status " + status + "."); return; } JSONObject result = new JSONObject(); result.put("ssid", options.optString("ssid")); result.put("platformMode", "suggestion"); result.put("saved", true); result.put("status", status); result.put("requiresUserApproval", true); callbackContext.success(result); } catch (Exception ex) { sendError(callbackContext, "WIFI_SAVE_FAILED", ex.getMessage()); } } private void saveLegacyWifi(JSONObject options, CallbackContext callbackContext) { WifiManager wifiManager = wifiManager(); if (wifiManager == null) { sendError(callbackContext, "WIFI_UNAVAILABLE", "WiFi manager is not available."); return; } int networkId = addOrUpdateLegacyNetwork(wifiManager, options); if (networkId < 0) { sendError(callbackContext, "WIFI_SAVE_FAILED", "Unable to add WiFi network configuration."); return; } wifiManager.saveConfiguration(); try { JSONObject result = new JSONObject(); result.put("ssid", options.optString("ssid")); result.put("platformMode", "legacy"); result.put("networkId", networkId); result.put("saved", true); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } @TargetApi(Build.VERSION_CODES.Q) private void connectWithSpecifier(final JSONObject options, final CallbackContext callbackContext) { final ConnectivityManager connectivityManager = connectivityManager(); if (connectivityManager == null) { sendError(callbackContext, "NETWORK_UNAVAILABLE", "Connectivity manager is not available."); return; } try { stopNetworkRequest(); WifiNetworkSpecifier specifier = buildSpecifier(options); NetworkRequest request = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .setNetworkSpecifier(specifier) .build(); networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { connectivityManager.bindProcessToNetwork(network); } else { ConnectivityManager.setProcessDefaultNetwork(network); } try { JSONObject result = new JSONObject(); result.put("ssid", options.optString("ssid")); result.put("platformMode", "specifier"); result.put("connected", true); result.put("temporaryConnection", true); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } @Override public void onUnavailable() { sendError(callbackContext, "WIFI_CONNECT_FAILED", "Requested WiFi network is unavailable or the user rejected the connection."); } @Override public void onLost(Network network) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { connectivityManager.bindProcessToNetwork(null); } else { ConnectivityManager.setProcessDefaultNetwork(null); } } }; connectivityManager.requestNetwork(request, networkCallback); } catch (Exception ex) { sendError(callbackContext, "WIFI_CONNECT_FAILED", ex.getMessage()); } } private void connectLegacyWifi(JSONObject options, CallbackContext callbackContext) { WifiManager wifiManager = wifiManager(); if (wifiManager == null) { sendError(callbackContext, "WIFI_UNAVAILABLE", "WiFi manager is not available."); return; } int networkId = addOrUpdateLegacyNetwork(wifiManager, options); if (networkId < 0) { sendError(callbackContext, "WIFI_CONNECT_FAILED", "Unable to add WiFi network configuration."); return; } boolean enabled = wifiManager.enableNetwork(networkId, true); boolean reconnecting = wifiManager.reconnect(); try { JSONObject result = new JSONObject(); result.put("ssid", options.optString("ssid")); result.put("platformMode", "legacy"); result.put("networkId", networkId); result.put("enabled", enabled); result.put("reconnecting", reconnecting); callbackContext.success(result); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } private void stopNetworkRequest() { ConnectivityManager connectivityManager = connectivityManager(); if (connectivityManager != null && networkCallback != null) { try { connectivityManager.unregisterNetworkCallback(networkCallback); } catch (RuntimeException ignored) { Log.d(TAG, "Network callback was already unregistered."); } } if (connectivityManager != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { connectivityManager.bindProcessToNetwork(null); } else { ConnectivityManager.setProcessDefaultNetwork(null); } } networkCallback = null; } @TargetApi(Build.VERSION_CODES.Q) private WifiNetworkSuggestion buildSuggestion(JSONObject options) { WifiNetworkSuggestion.Builder builder = new WifiNetworkSuggestion.Builder() .setSsid(options.optString("ssid")); if (options.optBoolean("hiddenSsid", false)) { builder.setIsHiddenSsid(true); } applySuggestionSecurity(builder, options); return builder.build(); } @TargetApi(Build.VERSION_CODES.Q) private WifiNetworkSpecifier buildSpecifier(JSONObject options) { WifiNetworkSpecifier.Builder builder = new WifiNetworkSpecifier.Builder() .setSsid(options.optString("ssid")); if (options.optBoolean("hiddenSsid", false)) { builder.setIsHiddenSsid(true); } String bssid = options.optString("bssid", ""); if (!TextUtils.isEmpty(bssid)) { builder.setBssid(android.net.MacAddress.fromString(bssid)); } applySpecifierSecurity(builder, options); return builder.build(); } @TargetApi(Build.VERSION_CODES.Q) private void applySuggestionSecurity(WifiNetworkSuggestion.Builder builder, JSONObject options) { String security = options.optString("security", "").toLowerCase(Locale.US); String password = options.optString("password", ""); if (TextUtils.isEmpty(password) || "open".equals(security)) { return; } if ("wpa3".equals(security) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { builder.setWpa3Passphrase(password); return; } builder.setWpa2Passphrase(password); } @TargetApi(Build.VERSION_CODES.Q) private void applySpecifierSecurity(WifiNetworkSpecifier.Builder builder, JSONObject options) { String security = options.optString("security", "").toLowerCase(Locale.US); String password = options.optString("password", ""); if (TextUtils.isEmpty(password) || "open".equals(security)) { return; } if ("wpa3".equals(security) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { builder.setWpa3Passphrase(password); return; } builder.setWpa2Passphrase(password); } private int addOrUpdateLegacyNetwork(WifiManager wifiManager, JSONObject options) { String ssid = options.optString("ssid"); int existingNetworkId = findLegacyNetworkId(wifiManager, ssid); WifiConfiguration configuration = buildLegacyConfiguration(options); if (existingNetworkId >= 0) { configuration.networkId = existingNetworkId; int updated = wifiManager.updateNetwork(configuration); return updated >= 0 ? updated : existingNetworkId; } return wifiManager.addNetwork(configuration); } private WifiConfiguration buildLegacyConfiguration(JSONObject options) { String security = options.optString("security", "").toLowerCase(Locale.US); String password = options.optString("password", ""); WifiConfiguration configuration = new WifiConfiguration(); configuration.SSID = quote(options.optString("ssid")); configuration.hiddenSSID = options.optBoolean("hiddenSsid", false); configuration.status = WifiConfiguration.Status.ENABLED; if (TextUtils.isEmpty(password) || "open".equals(security)) { configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); return configuration; } if ("wep".equals(security)) { configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); configuration.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); configuration.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); configuration.wepKeys[0] = quote(password); configuration.wepTxKeyIndex = 0; return configuration; } configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); configuration.preSharedKey = quote(password); return configuration; } private int findLegacyNetworkId(WifiManager wifiManager, String ssid) { try { List networks = wifiManager.getConfiguredNetworks(); if (networks == null) { return -1; } String quotedSsid = quote(ssid); for (WifiConfiguration network : networks) { if (quotedSsid.equals(network.SSID)) { return network.networkId; } } } catch (SecurityException ex) { Log.w(TAG, "Missing permission to read configured networks.", ex); } return -1; } private JSONObject normalizeWifiOptions(JSONObject options) { JSONObject normalized = options == null ? new JSONObject() : options; try { if (!normalized.has("security")) { normalized.put("security", TextUtils.isEmpty(normalized.optString("password", "")) ? "open" : "wpa2"); } } catch (JSONException ignored) { Log.w(TAG, "Unable to normalize WiFi options."); } return normalized; } private String validateWifiOptions(JSONObject options) { if (TextUtils.isEmpty(options.optString("ssid", ""))) { return "ssid is required."; } String security = options.optString("security", "open").toLowerCase(Locale.US); if (!"open".equals(security) && TextUtils.isEmpty(options.optString("password", ""))) { return "password is required for secured WiFi networks."; } return null; } private boolean hasWifiPermissions() { return missingWifiPermissions().length() == 0; } private JSONArray missingWifiPermissions() { JSONArray permissions = new JSONArray(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !PermissionHelper.hasPermission(this, Manifest.permission.NEARBY_WIFI_DEVICES)) { permissions.put(Manifest.permission.NEARBY_WIFI_DEVICES); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) { permissions.put(Manifest.permission.ACCESS_FINE_LOCATION); } return permissions; } private JSONObject biometricAvailability() throws JSONException { JSONObject result = new JSONObject(); result.put("available", false); result.put("biometryType", "none"); Context context = cordova.getActivity(); KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); if (keyguardManager == null || !keyguardManager.isKeyguardSecure()) { result.put("code", "NOT_ENROLLED"); result.put("message", "No secure lock screen or biometric credential is enrolled."); return result; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { BiometricManager biometricManager = (BiometricManager) context.getSystemService(BiometricManager.class); int code = biometricManager == null ? BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE : biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK); result.put("platformCode", code); if (code == BiometricManager.BIOMETRIC_SUCCESS) { result.put("available", true); result.put("biometryType", detectAndroidBiometryType(context)); result.put("code", "OK"); result.put("message", "Biometric authentication is available."); } else { result.put("code", "BIOMETRIC_UNAVAILABLE"); result.put("message", "Biometric authentication is unavailable. Android code: " + code); } return result; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { BiometricManager biometricManager = (BiometricManager) context.getSystemService(BiometricManager.class); int code = biometricManager == null ? BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE : biometricManager.canAuthenticate(); result.put("platformCode", code); if (code == BiometricManager.BIOMETRIC_SUCCESS) { result.put("available", true); result.put("biometryType", detectAndroidBiometryType(context)); result.put("code", "OK"); result.put("message", "Biometric authentication is available."); } else { result.put("code", "BIOMETRIC_UNAVAILABLE"); result.put("message", "Biometric authentication is unavailable. Android code: " + code); } return result; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (!hasBiometricFeature(context)) { result.put("code", "BIOMETRIC_UNAVAILABLE"); result.put("message", "No biometric hardware is available."); return result; } result.put("available", true); result.put("biometryType", detectAndroidBiometryType(context)); result.put("code", "OK"); result.put("message", "Biometric authentication is available."); return result; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { FingerprintManager fingerprintManager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); if (fingerprintManager != null && fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) { result.put("available", true); result.put("biometryType", "fingerprint"); result.put("code", "OK"); result.put("message", "Fingerprint authentication is available."); } else { result.put("code", "BIOMETRIC_UNAVAILABLE"); result.put("message", "Fingerprint authentication is unavailable."); } return result; } result.put("code", "UNSUPPORTED_ANDROID_VERSION"); result.put("message", "Biometric authentication is not supported on this Android version."); return result; } private boolean hasBiometricFeature(Context context) { PackageManager packageManager = context.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE) || packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS))) { return true; } return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); } @TargetApi(Build.VERSION_CODES.P) private void showBiometricPrompt(JSONObject options, final CallbackContext callbackContext) { Activity activity = cordova.getActivity(); Executor executor = activity.getMainExecutor(); String title = options.optString("title", "Unlock"); String subtitle = options.optString("subtitle", ""); String description = options.optString("description", ""); String negativeButton = options.optString("cancelText", "Cancel"); boolean allowDeviceCredential = options.optBoolean("allowDeviceCredential", true); BiometricPrompt.Builder builder = new BiometricPrompt.Builder(activity).setTitle(title); if (!TextUtils.isEmpty(subtitle)) { builder.setSubtitle(subtitle); } if (!TextUtils.isEmpty(description)) { builder.setDescription(description); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && allowDeviceCredential) { builder.setDeviceCredentialAllowed(true); } else { builder.setNegativeButton(negativeButton, executor, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { sendError(callbackContext, "AUTHENTICATION_CANCELLED", "User cancelled biometric authentication."); } }); } BiometricPrompt prompt = builder.build(); CancellationSignal cancellationSignal = new CancellationSignal(); prompt.authenticate(cancellationSignal, executor, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authenticationResult) { try { JSONObject result = new JSONObject(); result.put("unlocked", true); result.put("biometryType", detectAndroidBiometryType(cordova.getActivity())); callbackContext.success(result); JSONObject event = new JSONObject(result.toString()); event.put("type", "unlock"); sendEventToJs(event); } catch (JSONException ex) { sendError(callbackContext, "JSON_ERROR", ex.getMessage()); } } @Override public void onAuthenticationError(int errorCode, CharSequence errString) { sendError(callbackContext, "AUTHENTICATION_ERROR", String.valueOf(errString)); } @Override public void onAuthenticationFailed() { Log.d(TAG, "Biometric authentication attempt failed."); } }); } private String detectAndroidBiometryType(Context context) { PackageManager packageManager = context.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { return "face"; } if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { return "iris"; } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { return "fingerprint"; } return "biometric"; } private void completeFirebaseTask(Task task, CallbackContext callbackContext) { if (!task.isSuccessful()) { Exception exception = task.getException(); sendError(callbackContext, "FIREBASE_TASK_FAILED", exception == null ? "Firebase task failed." : exception.getMessage()); return; } callbackContext.success(); } private boolean ensureFirebaseInitialized(boolean logMissingConfig) { Context context = cordova.getActivity().getApplicationContext(); if (!FirebaseApp.getApps(context).isEmpty()) { return true; } try { ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); Bundle metaData = appInfo.metaData; String appId = metadataString(metaData, "bluememe.firebase.app_id"); String apiKey = metadataString(metaData, "bluememe.firebase.api_key"); String projectId = metadataString(metaData, "bluememe.firebase.project_id"); String senderId = metadataString(metaData, "bluememe.firebase.sender_id"); String storageBucket = metadataString(metaData, "bluememe.firebase.storage_bucket"); if (TextUtils.isEmpty(appId) || TextUtils.isEmpty(apiKey) || TextUtils.isEmpty(senderId)) { if (logMissingConfig) { Log.w(TAG, "Firebase options are missing."); } return false; } FirebaseOptions.Builder builder = new FirebaseOptions.Builder() .setApplicationId(appId) .setApiKey(apiKey) .setGcmSenderId(senderId); if (!TextUtils.isEmpty(projectId)) { builder.setProjectId(projectId); } if (!TextUtils.isEmpty(storageBucket)) { builder.setStorageBucket(storageBucket); } FirebaseApp.initializeApp(context, builder.build()); return true; } catch (Exception ex) { Log.w(TAG, "Unable to initialize Firebase.", ex); return false; } } private void registerEventReceiver() { if (eventReceiver != null) { return; } final Context context = cordova.getActivity().getApplicationContext(); eventReceiver = new BroadcastReceiver() { @Override public void onReceive(Context receiverContext, Intent intent) { String eventJson = intent.getStringExtra(EXTRA_EVENT); if (TextUtils.isEmpty(eventJson)) { return; } try { sendEventToJs(new JSONObject(eventJson)); } catch (JSONException ex) { Log.w(TAG, "Invalid event payload.", ex); } } }; IntentFilter filter = new IntentFilter(eventAction(context)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.registerReceiver(eventReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } else { context.registerReceiver(eventReceiver, filter); } } private void unregisterEventReceiver() { if (eventReceiver == null) { return; } try { cordova.getActivity().getApplicationContext().unregisterReceiver(eventReceiver); } catch (RuntimeException ignored) { Log.d(TAG, "Event receiver was already unregistered."); } eventReceiver = null; } private void captureLaunchIntent(Intent intent) { JSONObject notification = notificationFromIntent(intent); if (notification == null) { return; } saveInitialNotification(cordova.getActivity(), notification); try { JSONObject event = new JSONObject(); event.put("type", "notificationOpened"); event.put("notification", notification); sendEventToJs(event); } catch (JSONException ex) { Log.w(TAG, "Unable to dispatch launch notification.", ex); } } private JSONObject notificationFromIntent(Intent intent) { if (intent == null || intent.getExtras() == null) { return null; } Bundle extras = intent.getExtras(); String payload = extras.getString(EXTRA_PAYLOAD); if (!TextUtils.isEmpty(payload)) { try { return new JSONObject(payload); } catch (JSONException ex) { Log.w(TAG, "Invalid notification payload extra.", ex); } } boolean looksLikeFcm = extras.containsKey("google.message_id") || extras.containsKey("gcm.message_id"); if (!looksLikeFcm) { return null; } return bundleToJson(extras); } private void sendEventToJs(JSONObject event) { if (notificationCallback == null) { return; } PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, event); pluginResult.setKeepCallback(true); notificationCallback.sendPluginResult(pluginResult); } private WifiManager wifiManager() { return (WifiManager) cordova.getActivity().getApplicationContext().getSystemService(Context.WIFI_SERVICE); } private ConnectivityManager connectivityManager() { return (ConnectivityManager) cordova.getActivity().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); } private static JSONObject permissionResult(String type, boolean granted, JSONArray missing) throws JSONException { JSONObject result = new JSONObject(); result.put("type", type); result.put("granted", granted); result.put("missing", missing); return result; } private boolean allGranted(int[] grantResults) { if (grantResults == null || grantResults.length == 0) { return false; } for (int grantResult : grantResults) { if (grantResult != PackageManager.PERMISSION_GRANTED) { return false; } } return true; } private static String[] jsonArrayToStringArray(JSONArray array) { List values = new ArrayList<>(); for (int i = 0; i < array.length(); i++) { String value = array.optString(i, null); if (!TextUtils.isEmpty(value)) { values.add(value); } } return values.toArray(new String[0]); } private static String quote(String value) { if (value == null) { return "\"\""; } if (value.startsWith("\"") && value.endsWith("\"")) { return value; } return "\"" + value + "\""; } private static String metadataString(Bundle bundle, String key) { if (bundle == null || !bundle.containsKey(key)) { return ""; } Object value = bundle.get(key); return value == null ? "" : String.valueOf(value); } static String eventAction(Context context) { return context.getPackageName() + ACTION_SUFFIX; } static void publishEvent(Context context, JSONObject event) { Intent intent = new Intent(eventAction(context)); intent.setPackage(context.getPackageName()); intent.putExtra(EXTRA_EVENT, event.toString()); context.sendBroadcast(intent); } static void saveToken(Context context, String token) { if (TextUtils.isEmpty(token)) { return; } prefs(context).edit().putString(KEY_FCM_TOKEN, token).apply(); } static void saveInitialNotification(Context context, JSONObject notification) { if (notification == null) { return; } prefs(context).edit().putString(KEY_INITIAL_NOTIFICATION, notification.toString()).apply(); } static SharedPreferences prefs(Context context) { return context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); } static JSONObject bundleToJson(Bundle bundle) { JSONObject json = new JSONObject(); if (bundle == null) { return json; } for (String key : bundle.keySet()) { try { Object value = bundle.get(key); if (value instanceof Bundle) { json.put(key, bundleToJson((Bundle) value)); } else { json.put(key, value == null ? JSONObject.NULL : String.valueOf(value)); } } catch (JSONException ex) { Log.w(TAG, "Unable to add bundle value for key " + key + ".", ex); } } return json; } private void sendError(CallbackContext callbackContext, String code, String message) { try { JSONObject error = new JSONObject(); error.put("code", code); error.put("message", message == null ? "" : message); callbackContext.error(error); } catch (JSONException ex) { callbackContext.error(code + ": " + message); } } }