Files
bluememe-cordova-utils/src/android/BluememeUtils.java
T
2026-04-17 10:36:57 +09:00

1187 lines
48 KiB
Java

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<String>() {
@Override
public void onComplete(Task<String> 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<Void>() {
@Override
public void onComplete(Task<Void> 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<Void>() {
@Override
public void onComplete(Task<Void> 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<WifiConfiguration> 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<String> 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);
}
}
}