diff --git a/package-lock.json b/package-lock.json
index 69c700c01..3a84e30e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -114,6 +114,7 @@
"cordova-plugin-advanced-http": "file:src/plugins/cordova-plugin-advanced-http",
"cordova-plugin-browser": "file:src/plugins/browser",
"cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo",
+ "cordova-plugin-crashhandler": "file:src/plugins/cordova-plugin-crashhandler",
"cordova-plugin-device": "^2.1.0",
"cordova-plugin-file": "^8.1.3",
"cordova-plugin-ftp": "file:src/plugins/ftp",
@@ -8271,6 +8272,10 @@
"resolved": "src/plugins/cordova-plugin-buildinfo",
"link": true
},
+ "node_modules/cordova-plugin-crashhandler": {
+ "resolved": "src/plugins/cordova-plugin-crashhandler",
+ "link": true
+ },
"node_modules/cordova-plugin-device": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.1.0.tgz",
@@ -18508,6 +18513,11 @@
}
}
},
+ "src/plugins/cordova-plugin-crashhandler": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
"src/plugins/custom-tabs": {
"name": "com.foxdebug.acode.rk.customtabs",
"version": "1.0.0",
diff --git a/package.json b/package.json
index a35ff6fd0..c2a0cd908 100644
--- a/package.json
+++ b/package.json
@@ -42,9 +42,8 @@
"cordova-plugin-iap": {},
"com.foxdebug.acode.rk.customtabs": {},
"cordova-plugin-system": {},
- "cordova-plugin-advanced-http": {
- "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1"
- }
+ "cordova-plugin-crashhandler": {},
+ "cordova-plugin-advanced-http": {}
},
"platforms": [
"android"
@@ -88,6 +87,7 @@
"cordova-plugin-advanced-http": "file:src/plugins/cordova-plugin-advanced-http",
"cordova-plugin-browser": "file:src/plugins/browser",
"cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo",
+ "cordova-plugin-crashhandler": "file:src/plugins/cordova-plugin-crashhandler",
"cordova-plugin-device": "^2.1.0",
"cordova-plugin-file": "^8.1.3",
"cordova-plugin-ftp": "file:src/plugins/ftp",
diff --git a/src/plugins/cordova-plugin-crashhandler/package.json b/src/plugins/cordova-plugin-crashhandler/package.json
new file mode 100644
index 000000000..15accad6f
--- /dev/null
+++ b/src/plugins/cordova-plugin-crashhandler/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "cordova-plugin-crashhandler",
+ "version": "1.0.0",
+ "description": "Dedicated Cordova plugin for crash handling in Acode, supporting native and JS crashes even when the app is completely unrecoverable.",
+ "cordova": {
+ "id": "cordova-plugin-crashhandler",
+ "platforms": [
+ "android"
+ ]
+ },
+ "keywords": [
+ "ecosystem:cordova",
+ "cordova-android",
+ "crash",
+ "handler"
+ ],
+ "author": "Rohit Kushvaha",
+ "license": "MIT"
+}
diff --git a/src/plugins/cordova-plugin-crashhandler/plugin.xml b/src/plugins/cordova-plugin-crashhandler/plugin.xml
new file mode 100644
index 000000000..89ea176ba
--- /dev/null
+++ b/src/plugins/cordova-plugin-crashhandler/plugin.xml
@@ -0,0 +1,32 @@
+
+
+ CrashHandler
+ Dedicated crash handling plugin for Android
+ MIT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java
new file mode 100644
index 000000000..e9cd989e9
--- /dev/null
+++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java
@@ -0,0 +1,525 @@
+package com.foxdebug.crashhandler;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.webkit.WebView;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import org.json.JSONObject;
+
+public class CrashActivity extends Activity {
+
+ private String errorType;
+ private String errorMessage;
+ private String stackTrace;
+ private String fullReport;
+
+ private int colorPrimaryBg;
+ private int colorSecondaryBg;
+ private int colorPrimaryText;
+ private int colorSecondaryText;
+ private int colorLinkText;
+ private int colorBorder;
+ private int colorTraceBg;
+ private int colorMetaLabel;
+ private int colorMetaValue;
+ private int colorButtonPrimaryBg;
+ private int colorButtonPrimaryText;
+ private int colorButtonSecondaryBg;
+ private int colorButtonSecondaryText;
+ private boolean isDarkTheme;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ loadThemeColors();
+ applySystemBarColors();
+
+ Intent intent = getIntent();
+ errorType = intent.getStringExtra("error_type");
+ if (errorType == null) errorType = "Unexpected Crash";
+ errorMessage = intent.getStringExtra("error_message");
+ if (errorMessage == null) errorMessage = "No error message provided";
+ stackTrace = intent.getStringExtra("stack_trace");
+ if (stackTrace == null) stackTrace = "No stack trace details available.";
+
+ String appVersion = "Unknown";
+ String appBuild = "Unknown";
+ try {
+ PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ appVersion = pInfo.versionName;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ appBuild = String.valueOf(pInfo.getLongVersionCode());
+ } else {
+ appBuild = String.valueOf(pInfo.versionCode);
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+
+ String deviceName = Build.MANUFACTURER + " " + Build.MODEL;
+ String androidVersion = Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ")";
+ String webViewVersion = getWebViewVersion();
+ String appLanguage = getAppLanguage();
+
+ fullReport = "Acode Crash Report\n" +
+ "==================\n" +
+ "WebView Version: " + webViewVersion + "\n" +
+ "App Language: " + appLanguage + "\n" +
+ "Error Message: " + errorMessage + "\n" +
+ "App Version: " + appVersion + " (" + appBuild + ")\n" +
+ "Device: " + deviceName + "\n" +
+ "Android Version: " + androidVersion + "\n\n" +
+ "Stack Trace:\n" +
+ stackTrace;
+
+ ScrollView mainScrollView = new ScrollView(this);
+ mainScrollView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ mainScrollView.setBackgroundColor(colorPrimaryBg);
+ mainScrollView.setFillViewport(true);
+
+ LinearLayout rootLayout = new LinearLayout(this);
+ rootLayout.setOrientation(LinearLayout.VERTICAL);
+ LinearLayout.LayoutParams rootParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ int padding = dp(20);
+ rootLayout.setPadding(padding, padding, padding, padding);
+ rootLayout.setLayoutParams(rootParams);
+
+ TextView titleView = new TextView(this);
+ titleView.setText("Acode Crashed");
+ titleView.setTextSize(24);
+ titleView.setTextColor(colorPrimaryText);
+ titleView.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD));
+ LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ titleParams.setMargins(0, dp(10), 0, dp(8));
+ titleView.setLayoutParams(titleParams);
+ rootLayout.addView(titleView);
+
+ TextView descView = new TextView(this);
+ descView.setText("An unrecoverable exception occurred in Acode's native system. The application details and exception logs have been recorded below.");
+ descView.setTextSize(14);
+ descView.setTextColor(colorSecondaryText);
+ LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ descParams.setMargins(0, 0, 0, dp(20));
+ descView.setLayoutParams(descParams);
+ rootLayout.addView(descView);
+
+ TextView metaTitleView = new TextView(this);
+ metaTitleView.setText("DEVICE & APP INFO");
+ metaTitleView.setTextSize(11);
+ metaTitleView.setTextColor(colorLinkText);
+ metaTitleView.setTypeface(Typeface.create("sans-serif", Typeface.BOLD));
+ LinearLayout.LayoutParams metaTitleParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ metaTitleParams.setMargins(0, 0, 0, dp(6));
+ metaTitleView.setLayoutParams(metaTitleParams);
+ rootLayout.addView(metaTitleView);
+
+ LinearLayout metaCard = new LinearLayout(this);
+ metaCard.setOrientation(LinearLayout.VERTICAL);
+ metaCard.setPadding(dp(16), dp(16), dp(16), dp(16));
+
+ GradientDrawable cardBg = new GradientDrawable();
+ cardBg.setColor(colorSecondaryBg);
+ cardBg.setCornerRadius(dp(4));
+ cardBg.setStroke(dp(1), colorBorder);
+ metaCard.setBackground(cardBg);
+
+ LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ cardParams.setMargins(0, 0, 0, dp(20));
+ metaCard.setLayoutParams(cardParams);
+
+ metaCard.addView(createMetaRow("App Version", appVersion + " (" + appBuild + ")"));
+ metaCard.addView(createMetaRow("Device", deviceName));
+ metaCard.addView(createMetaRow("Android OS", androidVersion));
+ metaCard.addView(createMetaRow("WebView", webViewVersion));
+ metaCard.addView(createMetaRow("App Language", appLanguage));
+ rootLayout.addView(metaCard);
+
+ TextView logsTitleView = new TextView(this);
+ logsTitleView.setText("STACK TRACE");
+ logsTitleView.setTextSize(11);
+ logsTitleView.setTextColor(colorLinkText);
+ logsTitleView.setTypeface(Typeface.create("sans-serif", Typeface.BOLD));
+ LinearLayout.LayoutParams logsTitleParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ logsTitleParams.setMargins(0, 0, 0, dp(6));
+ logsTitleView.setLayoutParams(logsTitleParams);
+ rootLayout.addView(logsTitleView);
+
+ LinearLayout traceCard = new LinearLayout(this);
+ traceCard.setOrientation(LinearLayout.VERTICAL);
+ traceCard.setPadding(dp(12), dp(12), dp(12), dp(12));
+ GradientDrawable traceBg = new GradientDrawable();
+ traceBg.setColor(colorTraceBg);
+ traceBg.setCornerRadius(dp(4));
+ traceBg.setStroke(dp(1), colorBorder);
+ traceCard.setBackground(traceBg);
+
+ LinearLayout.LayoutParams traceCardParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dp(280));
+ traceCardParams.setMargins(0, 0, 0, dp(24));
+ traceCard.setLayoutParams(traceCardParams);
+
+ ScrollView traceVerticalScroll = new ScrollView(this);
+ HorizontalScrollView traceHorizontalScroll = new HorizontalScrollView(this);
+
+ TextView traceView = new TextView(this);
+ traceView.setText(stackTrace);
+ traceView.setTextSize(12);
+ traceView.setTextColor(colorSecondaryText);
+ traceView.setTypeface(Typeface.MONOSPACE);
+ traceView.setHorizontallyScrolling(true);
+ traceView.setTextIsSelectable(true);
+
+ traceHorizontalScroll.addView(traceView);
+ traceVerticalScroll.addView(traceHorizontalScroll);
+ traceCard.addView(traceVerticalScroll);
+ rootLayout.addView(traceCard);
+
+ LinearLayout buttonsLayout = new LinearLayout(this);
+ buttonsLayout.setOrientation(LinearLayout.VERTICAL);
+ LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ buttonsLayout.setLayoutParams(buttonsParams);
+
+ TextView btnRestart = createButton("Restart Acode", colorButtonPrimaryText, colorButtonPrimaryBg, false);
+ btnRestart.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent restartIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
+ if (restartIntent != null) {
+ restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(restartIntent);
+ }
+ finish();
+ System.exit(0);
+ }
+ });
+ buttonsLayout.addView(btnRestart);
+
+ TextView btnCopy = createButton("Copy Error Details", colorButtonSecondaryText, colorButtonSecondaryBg, true);
+ btnCopy.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Acode Crash Log", fullReport);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(CrashActivity.this, "Copied report to clipboard!", Toast.LENGTH_SHORT).show();
+ }
+ });
+ buttonsLayout.addView(btnCopy);
+
+ TextView btnClose = createButton("Close", colorButtonSecondaryText, colorButtonSecondaryBg, true);
+ btnClose.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ System.exit(0);
+ }
+ });
+ buttonsLayout.addView(btnClose);
+
+ rootLayout.addView(buttonsLayout);
+ mainScrollView.addView(rootLayout);
+ setContentView(mainScrollView);
+ }
+
+ private void loadThemeColors() {
+ SharedPreferences prefs = null;
+ try {
+ prefs = getApplicationContext()
+ .getSharedPreferences("acode_theme", Context.MODE_PRIVATE);
+ } catch (Exception ignored) {}
+
+ colorPrimaryBg = getThemeColor(prefs, "primaryColor", "#23272a");
+ colorSecondaryBg = getThemeColor(prefs, "secondaryColor", "#2d3134");
+ colorPrimaryText = getThemeColor(prefs, "primaryTextColor", "#f5f5f5");
+ colorSecondaryText = getThemeColor(prefs, "secondaryTextColor", "#e4e4e4");
+ colorLinkText = getThemeColor(prefs, "linkTextColor", "#8ab4f8");
+ colorButtonPrimaryBg = getThemeColor(prefs, "activeColor", "#4285f4");
+ colorButtonPrimaryText = getThemeColor(prefs, "buttonTextColor", "#ffffff");
+ colorButtonSecondaryBg = colorSecondaryBg;
+ colorButtonSecondaryText = colorSecondaryText;
+
+ String themeType = "dark";
+ if (prefs != null) {
+ try {
+ themeType = prefs.getString("type", "dark");
+ } catch (Exception ignored) {}
+ }
+ isDarkTheme = !"light".equals(themeType);
+
+ colorBorder = deriveBorderColor(colorPrimaryBg, colorSecondaryBg);
+ colorTraceBg = deriveTraceBg(colorPrimaryBg);
+ colorMetaLabel = deriveMetaLabelColor(colorSecondaryText);
+ colorMetaValue = colorPrimaryText;
+ }
+
+ private int getThemeColor(SharedPreferences prefs, String key, String fallback) {
+ if (prefs == null) return safeParseColor(fallback);
+ try {
+ String value = prefs.getString(key, null);
+ if (value != null && !value.isEmpty()) {
+ return safeParseColor(value, fallback);
+ }
+ } catch (Exception ignored) {}
+ return safeParseColor(fallback);
+ }
+
+ private int safeParseColor(String colorStr) {
+ return safeParseColor(colorStr, "#000000");
+ }
+
+ private int safeParseColor(String colorStr, String fallback) {
+ try {
+ return Color.parseColor(colorStr);
+ } catch (Exception e) {
+ try {
+ return Color.parseColor(fallback);
+ } catch (Exception e2) {
+ return Color.BLACK;
+ }
+ }
+ }
+
+ private int deriveBorderColor(int primary, int secondary) {
+ int r = (Color.red(primary) + Color.red(secondary)) / 2;
+ int g = (Color.green(primary) + Color.green(secondary)) / 2;
+ int b = (Color.blue(primary) + Color.blue(secondary)) / 2;
+ if (isDarkTheme) {
+ r = Math.min(255, r + 20);
+ g = Math.min(255, g + 20);
+ b = Math.min(255, b + 20);
+ } else {
+ r = Math.max(0, r - 20);
+ g = Math.max(0, g - 20);
+ b = Math.max(0, b - 20);
+ }
+ return Color.rgb(r, g, b);
+ }
+
+ private int deriveTraceBg(int primaryBg) {
+ if (isDarkTheme) {
+ return Color.rgb(
+ Math.max(0, Color.red(primaryBg) - 12),
+ Math.max(0, Color.green(primaryBg) - 12),
+ Math.max(0, Color.blue(primaryBg) - 12));
+ }
+ return Color.rgb(
+ Math.min(255, Color.red(primaryBg) + 8),
+ Math.min(255, Color.green(primaryBg) + 8),
+ Math.min(255, Color.blue(primaryBg) + 8));
+ }
+
+ private int deriveMetaLabelColor(int secondaryText) {
+ return Color.argb(
+ 180,
+ Color.red(secondaryText),
+ Color.green(secondaryText),
+ Color.blue(secondaryText));
+ }
+
+ private void applySystemBarColors() {
+ try {
+ Window window = getWindow();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ window.setStatusBarColor(colorPrimaryBg);
+ window.setNavigationBarColor(colorPrimaryBg);
+ }
+ if (!isDarkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ View decorView = window.getDecorView();
+ decorView.setSystemUiVisibility(
+ decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ } catch (Exception ignored) {}
+ }
+
+ private View createMetaRow(String label, String value) {
+ LinearLayout row = new LinearLayout(this);
+ row.setOrientation(LinearLayout.HORIZONTAL);
+ LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ rowParams.setMargins(0, 0, 0, dp(6));
+ row.setLayoutParams(rowParams);
+
+ TextView labelView = new TextView(this);
+ labelView.setText(label + ": ");
+ labelView.setTextSize(13);
+ labelView.setTextColor(colorMetaLabel);
+ labelView.setTypeface(Typeface.DEFAULT_BOLD);
+ LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
+ dp(100),
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ labelView.setLayoutParams(labelParams);
+
+ TextView valView = new TextView(this);
+ valView.setText(value);
+ valView.setTextSize(13);
+ valView.setTextColor(colorMetaValue);
+ LinearLayout.LayoutParams valParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ valView.setLayoutParams(valParams);
+
+ row.addView(labelView);
+ row.addView(valView);
+ return row;
+ }
+
+ private TextView createButton(String text, int textColor, int bgColor, boolean hasBorder) {
+ final TextView btn = new TextView(this);
+ btn.setText(text);
+ btn.setTextSize(15);
+ btn.setTextColor(textColor);
+ btn.setGravity(Gravity.CENTER);
+ btn.setPadding(dp(16), dp(14), dp(16), dp(14));
+ btn.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD));
+
+ final GradientDrawable normalBg = new GradientDrawable();
+ normalBg.setColor(bgColor);
+ normalBg.setCornerRadius(dp(4));
+
+ if (hasBorder) {
+ normalBg.setStroke(dp(1), colorBorder);
+ }
+
+ btn.setBackground(normalBg);
+
+ LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ btnParams.setMargins(0, 0, 0, dp(12));
+ btn.setLayoutParams(btnParams);
+
+ btn.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ btn.setAlpha(0.7f);
+ } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
+ btn.setAlpha(1.0f);
+ }
+ return false;
+ }
+ });
+
+ return btn;
+ }
+
+ private String getWebViewVersion() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ try {
+ PackageInfo info = WebView.getCurrentWebViewPackage();
+ if (info != null) {
+ return info.versionName;
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+
+ try {
+ String[] packages = {
+ "com.google.android.webview",
+ "com.android.chrome",
+ "com.android.webview"
+ };
+ for (String pkg : packages) {
+ try {
+ PackageInfo info = getPackageManager().getPackageInfo(pkg, 0);
+ if (info != null) {
+ return info.versionName;
+ }
+ } catch (Exception ignored) {}
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+
+ try {
+ return System.getProperty("http.agent");
+ } catch (Exception e) {
+ // Ignore
+ }
+
+ return "Unknown";
+ }
+
+ private String getAppLanguage() {
+ String langCode = "en-us";
+ try {
+ File settingsFile = new File(getExternalFilesDir(null), "settings.json");
+ if (!settingsFile.exists()) {
+ settingsFile = new File(getFilesDir(), "settings.json");
+ }
+ if (settingsFile.exists()) {
+ FileInputStream fis = new FileInputStream(settingsFile);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ reader.close();
+ fis.close();
+
+ JSONObject json = new JSONObject(sb.toString());
+ if (json.has("lang")) {
+ langCode = json.getString("lang");
+ }
+ }
+ } catch (Exception e) {
+ // Ignore
+ }
+
+ return langCode;
+ }
+
+ private int dp(float value) {
+ return (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ value,
+ getResources().getDisplayMetrics()
+ );
+ }
+}
diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java
new file mode 100644
index 000000000..cc02d1cc5
--- /dev/null
+++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java
@@ -0,0 +1,66 @@
+package com.foxdebug.crashhandler;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+
+public class CrashHandler extends CordovaPlugin {
+
+ private static final String TAG = "CrashHandler";
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+ Log.d(TAG, "Initializing CrashHandler...");
+
+ Thread.setDefaultUncaughtExceptionHandler(
+ new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ try {
+ Log.e(TAG, "Uncaught native exception detected!", ex);
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ ex.printStackTrace(pw);
+ String stackTrace = sw.toString();
+
+ Context context = cordova
+ .getActivity()
+ .getApplicationContext();
+ Intent intent = new Intent(
+ context,
+ CrashActivity.class
+ );
+ intent.putExtra("error_type", "Native Crash");
+ intent.putExtra(
+ "error_message",
+ ex.getMessage() != null
+ ? ex.getMessage()
+ : ex.toString()
+ );
+ intent.putExtra("stack_trace", stackTrace);
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TASK
+ );
+ context.startActivity(intent);
+ } catch (Throwable e) {
+ Log.e(TAG, "Failed to launch CrashActivity", e);
+ } finally {
+ //Should we terminate the app? or let it run with faulty state?
+ android.os.Process.killProcess(
+ android.os.Process.myPid()
+ );
+ System.exit(10);
+ }
+ }
+ }
+ );
+ }
+}
diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java
index 917b7be82..c97ef0c77 100644
--- a/src/plugins/system/android/com/foxdebug/system/System.java
+++ b/src/plugins/system/android/com/foxdebug/system/System.java
@@ -2,39 +2,52 @@
import static android.os.Build.VERSION.SDK_INT;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
-import java.io.IOException;
+import android.Manifest;
import android.app.Activity;
import android.app.PendingIntent;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
import android.content.*;
import android.content.pm.*;
-import java.util.*;
+import android.content.res.Configuration;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageDecoder;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Environment;
import android.os.PowerManager;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.Base64;
+import android.util.Log;
+import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.inputmethod.InputMethodManager;
+import android.webkit.MimeTypeMap;
import android.webkit.WebView;
+import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.documentfile.provider.DocumentFile;
import com.foxdebug.system.Ui.Theme;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
@@ -42,9 +55,13 @@
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
+import java.util.*;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
@@ -53,2154 +70,2141 @@
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import android.content.Context;
-import android.net.Uri;
-import android.util.Log;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.IOException;
-import android.webkit.MimeTypeMap;
+public class System extends CordovaPlugin {
-// DocumentFile import (AndroidX library)
-import androidx.documentfile.provider.DocumentFile;
+ private static final String TAG = "SystemPlugin";
+
+ private CallbackContext requestPermissionCallback;
+ private Activity activity;
+ private Context context;
+ private int REQ_PERMISSIONS = 1;
+ private int REQ_PERMISSION = 2;
+ private int systemBarColor = 0xFF000000;
+ private Theme theme;
+ private CallbackContext intentHandler;
+ private CordovaWebView webView;
+ private String fileProviderAuthority;
+ private RewardPassManager rewardPassManager;
+
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+ this.context = cordova.getContext();
+ this.activity = cordova.getActivity();
+ this.webView = webView;
+ this.rewardPassManager = new RewardPassManager(this.context);
+ this.activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ setNativeContextMenuDisabled(false);
+ }
+ }
+ );
+ }
+
+ public boolean execute(
+ String action,
+ final JSONArray args,
+ final CallbackContext callbackContext
+ ) throws JSONException {
+ final String arg1 = args.optString(0);
+ final String arg2 = args.optString(1);
+ final String arg3 = args.optString(2);
+ final String arg4 = args.optString(3);
+ final String arg5 = args.optString(4);
+ final String arg6 = args.optString(5);
+
+ switch (action) {
+ case "get-webkit-info":
+ case "file-action":
+ case "checksumText":
+ case "is-powersave-mode":
+ case "get-app-info":
+ case "add-shortcut":
+ case "remove-shortcut":
+ case "pin-shortcut":
+ case "get-android-version":
+ case "request-permissions":
+ case "request-permission":
+ case "has-permission":
+ case "open-in-browser":
+ case "launch-app":
+ case "get-global-setting":
+ case "get-available-encodings":
+ case "decode":
+ case "encode":
+ case "copyToUri":
+ case "getInstaller":
+ case "compare-file-text":
+ case "compare-texts":
+ case "extractAsset":
+ case "pin-file-shortcut":
+ break;
+ case "get-configuration":
+ getConfiguration(callbackContext);
+ return true;
+ case "set-input-type":
+ setInputType(arg1);
+ callbackContext.success();
+ return true;
+ case "set-native-context-menu-disabled":
+ this.cordova.getActivity().runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ setNativeContextMenuDisabled(Boolean.parseBoolean(arg1));
+ callbackContext.success();
+ }
+ }
+ );
+ return true;
+ case "get-cordova-intent":
+ getCordovaIntent(callbackContext);
+ return true;
+ case "set-intent-handler":
+ setIntentHandler(callbackContext);
+ return true;
+ case "shareText":
+ String text = args.getString(0);
-// Java I/O imports
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.IOException;
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
+ cordova.getActivity().runOnUiThread(() -> {
+ try {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ cordova
+ .getActivity()
+ .startActivity(Intent.createChooser(shareIntent, "Share"));
+
+ callbackContext.success();
+ } catch (Exception e) {
+ callbackContext.error(e.getMessage());
+ }
+ });
+ return true;
+ case "set-ui-theme":
+ this.cordova.getActivity().runOnUiThread(
+ new Runnable() {
+ public void run() {
+ setUiTheme(arg1, args.optJSONObject(1), callbackContext);
+ }
+ }
+ );
+ return true;
+ case "clear-cache":
+ this.cordova.getActivity().runOnUiThread(
+ new Runnable() {
+ public void run() {
+ clearCache(callbackContext);
+ }
+ }
+ );
+ return true;
+ case "fileExists":
+ callbackContext.success(
+ fileExists(args.getString(0), args.getString(1)) ? 1 : 0
+ );
+ return true;
+ case "createSymlink":
+ boolean success = createSymlink(args.getString(0), args.getString(1));
+ callbackContext.success(success ? 1 : 0);
+ return true;
+ case "getNativeLibraryPath":
+ callbackContext.success(getNativeLibraryPath());
+ return true;
+ case "getFilesDir":
+ callbackContext.success(getFilesDir());
+ return true;
+ case "getRewardStatus":
+ callbackContext.success(rewardPassManager.getRewardStatus());
+ return true;
+ case "redeemReward":
+ callbackContext.success(
+ rewardPassManager.redeemReward(args.getString(0))
+ );
+ return true;
+ case "getParentPath":
+ callbackContext.success(getParentPath(args.getString(0)));
+ return true;
+ case "listChildren":
+ callbackContext.success(listChildren(args.getString(0)));
+ return true;
+ case "writeText": {
+ try {
+ String filePath = args.getString(0);
+ String content = args.getString(1);
-import android.os.Build;
-import android.os.Environment;
-import android.Manifest;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.provider.DocumentsContract;
-import android.provider.Settings;
+ Files.write(
+ Paths.get(filePath),
+ Collections.singleton(content),
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING
+ );
-import androidx.core.content.ContextCompat;
+ callbackContext.success("File written successfully");
+ } catch (Exception e) {
+ callbackContext.error("Failed to write file: " + e.getMessage());
+ }
+ return true;
+ }
+ case "getArch":
+ String arch;
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ arch = android.os.Build.SUPPORTED_ABIS[0];
+ } else {
+ arch = android.os.Build.CPU_ABI;
+ }
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.graphics.Canvas;
-import android.util.TypedValue;
+ callbackContext.success(arch);
+ return true;
+ case "requestStorageManager":
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ try {
+ Intent intent = new Intent(
+ Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
+ );
+ intent.setData(Uri.parse("package:" + context.getPackageName()));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ callbackContext.success("true");
+ } catch (Exception e) {
+ // Fallback to general settings if specific one fails
+ Intent intent = new Intent(
+ Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
+ );
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ callbackContext.success("true");
+ }
+ } else {
+ callbackContext.success("false"); // Not needed on Android < 11
+ }
+ return true;
+ case "hasGrantedStorageManager":
+ boolean granted;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ granted = Environment.isExternalStorageManager();
+ } else {
+ // Fallback for Android 10 and below
+ granted =
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED;
+ }
+ callbackContext.success(String.valueOf(granted));
+ return true;
+ case "isManageExternalStorageDeclared":
+ PackageManager pm = context.getPackageManager();
+ try {
+ PackageInfo info = pm.getPackageInfo(
+ context.getPackageName(),
+ PackageManager.GET_PERMISSIONS
+ );
+ String[] permissions = info.requestedPermissions;
+ String isDeclared = "false";
+
+ if (permissions != null) {
+ for (String perm : permissions) {
+ if (perm.equals("android.permission.MANAGE_EXTERNAL_STORAGE")) {
+ isDeclared = "true";
+ break;
+ }
+ }
+ }
+ callbackContext.success(isDeclared);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ callbackContext.error(e.toString());
+ }
-import android.provider.MediaStore;
-import android.graphics.Bitmap;
-import android.os.Build;
-import android.graphics.ImageDecoder;
+ return true;
+ case "mkdirs":
+ if (new File(args.getString(0)).mkdirs()) {
+ callbackContext.success();
+ } else {
+ callbackContext.error("mkdirs failed");
+ }
+ return true;
+ case "deleteFile":
+ if (new File(args.getString(0)).delete()) {
+ callbackContext.success();
+ } else {
+ callbackContext.error("delete failed");
+ }
+ return true;
+ case "setExec":
+ if (
+ new File(args.getString(0)).setExecutable(
+ Boolean.parseBoolean(args.getString(1))
+ )
+ ) {
+ callbackContext.success();
+ } else {
+ callbackContext.error("set exec failed");
+ }
+
+ return true;
+ default:
+ return false;
+ }
+ cordova.getThreadPool().execute(
+ new Runnable() {
+ public void run() {
+ switch (action) {
+ case "extractAsset":
+ try {
+ String assetName = args.getString(0);
+ String destinationPath = args.getString(1);
+ extractAsset(assetName, destinationPath, callbackContext);
+ } catch (Exception e) {
+ callbackContext.error(
+ "Failed to extract asset: " + e.getMessage()
+ );
+ }
+ return;
+ case "getInstaller":
+ try {
+ PackageManager pm = context.getPackageManager();
-import java.security.MessageDigest;
-import java.security.MessageDigest;
+ String installer;
-import android.content.pm.InstallSourceInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ InstallSourceInfo info = pm.getInstallSourceInfo(
+ context.getPackageName()
+ );
-import android.content.Intent;
+ installer = info.getInstallingPackageName();
+ } else {
+ installer = pm.getInstallerPackageName(
+ context.getPackageName()
+ );
+ }
-import org.apache.cordova.CallbackContext;
-import org.apache.cordova.CordovaPlugin;
+ callbackContext.success(installer);
+ } catch (Exception e) {
+ callbackContext.error(e.getMessage());
+ }
+ break;
+ case "copyToUri":
+ try {
+ //srcUri is a file
+ Uri srcUri = Uri.parse(args.getString(0));
-import org.json.JSONArray;
-import org.json.JSONException;
+ //destUri is a directory
+ Uri destUri = Uri.parse(args.getString(1));
-public class System extends CordovaPlugin {
- private static final String TAG = "SystemPlugin";
-
- private CallbackContext requestPermissionCallback;
- private Activity activity;
- private Context context;
- private int REQ_PERMISSIONS = 1;
- private int REQ_PERMISSION = 2;
- private int systemBarColor = 0xFF000000;
- private Theme theme;
- private CallbackContext intentHandler;
- private CordovaWebView webView;
- private String fileProviderAuthority;
- private RewardPassManager rewardPassManager;
-
- public void initialize(CordovaInterface cordova, CordovaWebView webView) {
- super.initialize(cordova, webView);
- this.context = cordova.getContext();
- this.activity = cordova.getActivity();
- this.webView = webView;
- this.rewardPassManager = new RewardPassManager(this.context);
- this.activity.runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- setNativeContextMenuDisabled(false);
- }
- }
- );
+ //create a file named this into the dest Directory and copy the srcUri into it
+ String fileName = args.getString(2);
+
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ // Open input stream from the source URI
+ if ("file".equalsIgnoreCase(srcUri.getScheme())) {
+ File file = new File(srcUri.getPath());
+ in = new FileInputStream(file);
+ } else {
+ in = context.getContentResolver().openInputStream(srcUri);
+ }
+
+ // Create the destination file using DocumentFile for better URI handling
+ DocumentFile destFile = null;
+
+ if ("file".equalsIgnoreCase(destUri.getScheme())) {
+ // Handle file:// scheme using DocumentFile
+ File destDir = new File(destUri.getPath());
+ if (!destDir.exists()) {
+ destDir.mkdirs(); // Create directory if it doesn't exist
+ }
+ DocumentFile destDocDir = DocumentFile.fromFile(destDir);
+
+ // Check if file already exists and delete it
+ DocumentFile existingFile = destDocDir.findFile(fileName);
+ if (existingFile != null && existingFile.exists()) {
+ existingFile.delete();
+ }
- // Set up global exception handler
- Thread.setDefaultUncaughtExceptionHandler(
- new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(Thread thread, Throwable ex) {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- ex.printStackTrace(pw);
- String stackTrace = sw.toString();
-
- String errorMsg = String.format(
- "Uncaught Exception: %s\nStack trace: %s",
- ex.getMessage(),
- stackTrace
+ // Create new file
+ String mimeType = getMimeTypeFromExtension(fileName);
+ destFile = destDocDir.createFile(mimeType, fileName);
+ } else {
+ // Handle content:// scheme using DocumentFile
+ DocumentFile destDocDir = DocumentFile.fromTreeUri(
+ context,
+ destUri
);
- sendLogToJavaScript("error", errorMsg);
+ if (
+ destDocDir == null ||
+ !destDocDir.exists() ||
+ !destDocDir.isDirectory()
+ ) {
+ callbackContext.error(
+ "Destination directory does not exist or is not accessible"
+ );
+ return;
+ }
- // rethrow to the default handler
- Thread.getDefaultUncaughtExceptionHandler()
- .uncaughtException(thread, ex);
- }
- }
- );
- }
+ // Check if file already exists and delete it
+ DocumentFile existingFile = destDocDir.findFile(fileName);
+ if (existingFile != null && existingFile.exists()) {
+ existingFile.delete();
+ }
+
+ // Create new file
+ String mimeType = getMimeTypeFromExtension(fileName);
+ destFile = destDocDir.createFile(mimeType, fileName);
+ }
+
+ if (destFile == null || !destFile.exists()) {
+ callbackContext.error("Failed to create destination file");
+ return;
+ }
- public boolean execute(
- String action,
- final JSONArray args,
- final CallbackContext callbackContext
- ) throws JSONException {
- final String arg1 = args.optString(0);
- final String arg2 = args.optString(1);
- final String arg3 = args.optString(2);
- final String arg4 = args.optString(3);
- final String arg5 = args.optString(4);
- final String arg6 = args.optString(5);
+ // Open output stream to the created file
+ out = context
+ .getContentResolver()
+ .openOutputStream(destFile.getUri());
- switch (action) {
+ if (in == null || out == null) {
+ callbackContext.error("uri streams are null");
+ return;
+ }
+
+ // Copy stream
+ byte[] buffer = new byte[8192];
+ int len;
+ while ((len = in.read(buffer)) > 0) {
+ out.write(buffer, 0, len);
+ }
+
+ out.flush();
+ callbackContext.success();
+ } catch (IOException e) {
+ e.printStackTrace();
+ callbackContext.error(e.toString());
+ } finally {
+ try {
+ if (in != null) in.close();
+ if (out != null) out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ callbackContext.error(e.toString());
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ callbackContext.error(e.toString());
+ }
+ break;
case "get-webkit-info":
+ getWebkitInfo(callbackContext);
+ break;
case "file-action":
- case "checksumText":
+ fileAction(arg1, arg2, arg3, arg4, callbackContext);
+ break;
case "is-powersave-mode":
+ isPowerSaveMode(callbackContext);
+ break;
case "get-app-info":
+ getAppInfo(callbackContext);
+ break;
+ case "pin-file-shortcut":
+ pinFileShortcut(args.optJSONObject(0), callbackContext);
+ break;
case "add-shortcut":
+ addShortcut(arg1, arg2, arg3, arg4, arg5, arg6, callbackContext);
+ break;
case "remove-shortcut":
+ removeShortcut(arg1, callbackContext);
+ break;
case "pin-shortcut":
+ pinShortcut(arg1, callbackContext);
+ break;
case "get-android-version":
+ getAndroidVersion(callbackContext);
+ break;
case "request-permissions":
+ requestPermissions(args.optJSONArray(0), callbackContext);
+ break;
case "request-permission":
+ requestPermission(arg1, callbackContext);
+ break;
case "has-permission":
+ hasPermission(arg1, callbackContext);
+ break;
case "open-in-browser":
+ openInBrowser(arg1, callbackContext);
+ break;
case "launch-app":
+ launchApp(arg1, arg2, args.optJSONObject(2), callbackContext);
+ break;
case "get-global-setting":
+ getGlobalSetting(arg1, callbackContext);
+ break;
case "get-available-encodings":
+ getAvailableEncodings(callbackContext);
+ break;
case "decode":
+ decode(arg1, arg2, callbackContext);
+ break;
case "encode":
- case "copyToUri":
- case "getInstaller":
+ encode(arg1, arg2, callbackContext);
+ break;
case "compare-file-text":
+ compareFileText(arg1, arg2, arg3, callbackContext);
+ break;
case "compare-texts":
- case "extractAsset":
- case "pin-file-shortcut":
- break;
- case "get-configuration":
- getConfiguration(callbackContext);
- return true;
- case "set-input-type":
- setInputType(arg1);
- callbackContext.success();
- return true;
- case "set-native-context-menu-disabled":
- this.cordova.getActivity()
- .runOnUiThread(
- new Runnable() {
- @Override
- public void run() {
- setNativeContextMenuDisabled(Boolean.parseBoolean(arg1));
- callbackContext.success();
- }
- }
- );
- return true;
- case "get-cordova-intent":
- getCordovaIntent(callbackContext);
- return true;
- case "set-intent-handler":
- setIntentHandler(callbackContext);
- return true;
-
- case "shareText":
- String text = args.getString(0);
-
- cordova.getActivity().runOnUiThread(() -> {
- try {
- Intent shareIntent = new Intent(Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.putExtra(Intent.EXTRA_TEXT, text);
-
- cordova.getActivity().startActivity(
- Intent.createChooser(shareIntent, "Share")
- );
-
- callbackContext.success();
-
- } catch (Exception e) {
- callbackContext.error(e.getMessage());
- }
- });
- return true;
- case "set-ui-theme":
- this.cordova.getActivity()
- .runOnUiThread(
- new Runnable() {
- public void run() {
- setUiTheme(arg1, args.optJSONObject(1), callbackContext);
- }
- }
- );
- return true;
- case "clear-cache":
- this.cordova.getActivity()
- .runOnUiThread(
- new Runnable() {
- public void run() {
- clearCache(callbackContext);
- }
- }
- );
- return true;
- case "fileExists":
- callbackContext.success(fileExists(args.getString(0), args.getString(1)) ? 1 : 0);
- return true;
-
- case "createSymlink":
- boolean success = createSymlink(args.getString(0), args.getString(1));
- callbackContext.success(success ? 1 : 0);
- return true;
-
- case "getNativeLibraryPath":
- callbackContext.success(getNativeLibraryPath());
- return true;
-
- case "getFilesDir":
- callbackContext.success(getFilesDir());
- return true;
- case "getRewardStatus":
- callbackContext.success(rewardPassManager.getRewardStatus());
- return true;
- case "redeemReward":
- callbackContext.success(rewardPassManager.redeemReward(args.getString(0)));
- return true;
-
- case "getParentPath":
- callbackContext.success(getParentPath(args.getString(0)));
- return true;
-
- case "listChildren":
- callbackContext.success(listChildren(args.getString(0)));
- return true;
- case "writeText":
- {
- try {
- String filePath = args.getString(0);
- String content = args.getString(1);
-
- Files.write(Paths.get(filePath),
- Collections.singleton(content),
- StandardOpenOption.CREATE,
- StandardOpenOption.TRUNCATE_EXISTING);
-
- callbackContext.success("File written successfully");
- } catch (Exception e) {
- callbackContext.error("Failed to write file: " + e.getMessage());
- }
- return true;
- }
+ compareTexts(arg1, arg2, callbackContext);
+ break;
+ case "checksumText":
+ cordova.getThreadPool().execute(() -> {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
- case "getArch":
- String arch;
+ byte[] hash = digest.digest(
+ args.getString(0).getBytes("UTF-8")
+ );
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- arch = android.os.Build.SUPPORTED_ABIS[0];
- } else {
- arch = android.os.Build.CPU_ABI;
- }
+ StringBuilder hexString = new StringBuilder();
- callbackContext.success(arch);
- return true;
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
- case "requestStorageManager":
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- try {
- Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
- intent.setData(Uri.parse("package:" + context.getPackageName()));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- callbackContext.success("true");
- } catch (Exception e) {
- // Fallback to general settings if specific one fails
- Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- callbackContext.success("true");
- }
- } else {
- callbackContext.success("false"); // Not needed on Android < 11
- }
- return true;
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
- case "hasGrantedStorageManager":
- boolean granted;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- granted = Environment.isExternalStorageManager();
- } else {
- // Fallback for Android 10 and below
- granted = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) == PackageManager.PERMISSION_GRANTED;
+ callbackContext.success(hexString.toString());
+ } catch (Exception e) {
+ callbackContext.error(e.getMessage());
}
- callbackContext.success(String.valueOf(granted));
- return true;
+ });
- case "isManageExternalStorageDeclared":
- PackageManager pm = context.getPackageManager();
- try {
- PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
- String[] permissions = info.requestedPermissions;
- String isDeclared = "false";
-
- if (permissions != null) {
- for (String perm: permissions) {
- if (perm.equals("android.permission.MANAGE_EXTERNAL_STORAGE")) {
- isDeclared = "true";
- break;
- }
- }
- }
- callbackContext.success(isDeclared);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ );
+
+ return true;
+ }
+
+ private void sendLogToJavaScript(String level, String message) {
+ final String js =
+ "if (typeof window.log === 'function')" +
+ " window.log(" +
+ JSONObject.quote(level) +
+ ", " +
+ JSONObject.quote(message) +
+ ");" +
+ "else" +
+ " console.log(" +
+ JSONObject.quote(level) +
+ ", " +
+ JSONObject.quote(message) +
+ ");";
+
+ cordova.getActivity().runOnUiThread(() -> {
+ try {
+ (
+ (android.webkit.WebView) webView.getEngine().getView()
+ ).evaluateJavascript(js, null);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to send log to JavaScript: " + e.getMessage());
+ }
+ });
+ }
+
+ // Helper method to determine MIME type using Android's built-in MimeTypeMap
+ private String getMimeTypeFromExtension(String fileName) {
+ String extension = "";
+ int lastDotIndex = fileName.lastIndexOf('.');
+ if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
+ extension = fileName.substring(lastDotIndex + 1).toLowerCase();
+ }
+
+ String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ extension
+ );
+ return mimeType != null ? mimeType : "application/octet-stream";
+ }
+
+ private void getConfiguration(CallbackContext callback) {
+ try {
+ JSONObject result = new JSONObject();
+ Configuration config = context.getResources().getConfiguration();
+ InputMethodManager imm = (InputMethodManager) context.getSystemService(
+ Context.INPUT_METHOD_SERVICE
+ );
+ Method method = InputMethodManager.class.getMethod(
+ "getInputMethodWindowVisibleHeight"
+ );
+
+ result.put("isAcceptingText", imm.isAcceptingText());
+ result.put("keyboardHeight", method.invoke(imm));
+ result.put("locale", config.locale.toString());
+ result.put("fontScale", config.fontScale);
+ result.put("keyboard", config.keyboard);
+ result.put("keyboardHidden", config.keyboardHidden);
+ result.put("hardKeyboardHidden", config.hardKeyboardHidden);
+ result.put("navigationHidden", config.navigationHidden);
+ result.put("navigation", config.navigation);
+ result.put("orientation", config.orientation);
+ callback.success(result);
+ } catch (
+ JSONException
+ | NoSuchMethodException
+ | IllegalAccessException
+ | InvocationTargetException error
+ ) {
+ callback.error(error.toString());
+ }
+ }
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- callbackContext.error(e.toString());
- }
+ private void decode(
+ String content, // base64 encoded string
+ String charSetName,
+ CallbackContext callback
+ ) {
+ try {
+ byte[] bytes = Base64.decode(content, Base64.DEFAULT);
- return true;
- case "mkdirs":
- if (new File(args.getString(0)).mkdirs()) {
- callbackContext.success();
- } else {
- callbackContext.error("mkdirs failed");
- }
- return true;
- case "deleteFile":
- if (new File(args.getString(0)).delete()) {
- callbackContext.success();
- } else {
- callbackContext.error("delete failed");
- }
- return true;
- case "setExec":
- if (new File(args.getString(0)).setExecutable(Boolean.parseBoolean(args.getString(1)))) {
- callbackContext.success();
- } else {
- callbackContext.error("set exec failed");
- }
+ if (Charset.isSupported(charSetName) == false) {
+ callback.error("Charset not supported: " + charSetName);
+ return;
+ }
+
+ Charset charSet = Charset.forName(charSetName);
+ CharBuffer charBuffer = charSet.decode(ByteBuffer.wrap(bytes));
+ String result = String.valueOf(charBuffer);
+ callback.success(result);
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void encode(
+ String content, // string to encode
+ String charSetName,
+ CallbackContext callback
+ ) {
+ try {
+ if (Charset.isSupported(charSetName) == false) {
+ callback.error("Charset not supported: " + charSetName);
+ return;
+ }
+
+ Charset charSet = Charset.forName(charSetName);
+ ByteBuffer byteBuffer = charSet.encode(content);
+ byte[] bytes = new byte[byteBuffer.remaining()];
+ byteBuffer.get(bytes);
+ callback.success(bytes);
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ /**
+ * Compares file content with provided text.
+ * This method runs in a background thread to avoid blocking the UI.
+ *
+ * @param fileUri The URI of the file to read (file:// or content://)
+ * @param encoding The character encoding to use when reading the file
+ * @param currentText The text to compare against the file content
+ * @param callback Returns 1 if texts are different, 0 if same
+ */
+ private void compareFileText(
+ String fileUri,
+ String encoding,
+ String currentText,
+ CallbackContext callback
+ ) {
+ try {
+ if (fileUri == null || fileUri.isEmpty()) {
+ callback.error("File URI is required");
+ return;
+ }
- return true;
- default:
- return false;
- }
+ if (encoding == null || encoding.isEmpty()) {
+ encoding = "UTF-8";
+ }
- cordova
- .getThreadPool()
- .execute(
- new Runnable() {
- public void run() {
- switch (action) {
- case "extractAsset":
- try{
- String assetName = args.getString(0);
- String destinationPath = args.getString(1);
- extractAsset(assetName, destinationPath, callbackContext);
- }catch(Exception e){
- callbackContext.error("Failed to extract asset: " + e.getMessage());
-
- }
- return;
-
- case "getInstaller":
- try {
- PackageManager pm = context.getPackageManager();
-
- String installer;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- InstallSourceInfo info =
- pm.getInstallSourceInfo(context.getPackageName());
-
- installer = info.getInstallingPackageName();
- } else {
- installer = pm.getInstallerPackageName(
- context.getPackageName()
- );
- }
-
- callbackContext.success(installer);
-
- } catch (Exception e) {
- callbackContext.error(e.getMessage());
- }
- break;
- case "copyToUri":
- try {
- //srcUri is a file
- Uri srcUri = Uri.parse(args.getString(0));
-
- //destUri is a directory
- Uri destUri = Uri.parse(args.getString(1));
-
- //create a file named this into the dest Directory and copy the srcUri into it
- String fileName = args.getString(2);
-
- InputStream in = null;
- OutputStream out = null;
- try {
- // Open input stream from the source URI
- if ("file".equalsIgnoreCase(srcUri.getScheme())) {
- File file = new File(srcUri.getPath()); in = new FileInputStream(file);
- } else { in = context.getContentResolver().openInputStream(srcUri);
- }
-
- // Create the destination file using DocumentFile for better URI handling
- DocumentFile destFile = null;
-
- if ("file".equalsIgnoreCase(destUri.getScheme())) {
- // Handle file:// scheme using DocumentFile
- File destDir = new File(destUri.getPath());
- if (!destDir.exists()) {
- destDir.mkdirs(); // Create directory if it doesn't exist
- }
- DocumentFile destDocDir = DocumentFile.fromFile(destDir);
-
- // Check if file already exists and delete it
- DocumentFile existingFile = destDocDir.findFile(fileName);
- if (existingFile != null && existingFile.exists()) {
- existingFile.delete();
- }
-
- // Create new file
- String mimeType = getMimeTypeFromExtension(fileName);
- destFile = destDocDir.createFile(mimeType, fileName);
- } else {
- // Handle content:// scheme using DocumentFile
- DocumentFile destDocDir = DocumentFile.fromTreeUri(context, destUri);
-
- if (destDocDir == null || !destDocDir.exists() || !destDocDir.isDirectory()) {
- callbackContext.error("Destination directory does not exist or is not accessible");
- return;
- }
-
- // Check if file already exists and delete it
- DocumentFile existingFile = destDocDir.findFile(fileName);
- if (existingFile != null && existingFile.exists()) {
- existingFile.delete();
- }
-
- // Create new file
- String mimeType = getMimeTypeFromExtension(fileName);
- destFile = destDocDir.createFile(mimeType, fileName);
- }
-
- if (destFile == null || !destFile.exists()) {
- callbackContext.error("Failed to create destination file");
- return;
- }
-
- // Open output stream to the created file
- out = context.getContentResolver().openOutputStream(destFile.getUri());
-
- if ( in == null || out == null) {
- callbackContext.error("uri streams are null");
- return;
- }
-
- // Copy stream
- byte[] buffer = new byte[8192];
- int len;
- while ((len = in .read(buffer)) > 0) {
- out.write(buffer, 0, len);
- }
-
- out.flush();
- callbackContext.success();
- } catch (IOException e) {
- e.printStackTrace();
- callbackContext.error(e.toString());
- } finally {
- try {
- if ( in != null) in .close();
- if (out != null) out.close();
- } catch (IOException e) {
- e.printStackTrace();
- callbackContext.error(e.toString());
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- callbackContext.error(e.toString());
- }
- break;
- case "get-webkit-info":
- getWebkitInfo(callbackContext);
- break;
- case "file-action":
- fileAction(arg1, arg2, arg3, arg4, callbackContext);
- break;
- case "is-powersave-mode":
- isPowerSaveMode(callbackContext);
- break;
- case "get-app-info":
- getAppInfo(callbackContext);
- break;
- case "pin-file-shortcut":
- pinFileShortcut(args.optJSONObject(0), callbackContext);
- break;
- case "add-shortcut":
- addShortcut(
- arg1,
- arg2,
- arg3,
- arg4,
- arg5,
- arg6,
- callbackContext
- );
- break;
- case "remove-shortcut":
- removeShortcut(arg1, callbackContext);
- break;
- case "pin-shortcut":
- pinShortcut(arg1, callbackContext);
- break;
- case "get-android-version":
- getAndroidVersion(callbackContext);
- break;
- case "request-permissions":
- requestPermissions(args.optJSONArray(0), callbackContext);
- break;
- case "request-permission":
- requestPermission(arg1, callbackContext);
- break;
- case "has-permission":
- hasPermission(arg1, callbackContext);
- break;
- case "open-in-browser":
- openInBrowser(arg1, callbackContext);
- break;
- case "launch-app":
- launchApp(arg1, arg2, args.optJSONObject(2), callbackContext);
- break;
- case "get-global-setting":
- getGlobalSetting(arg1, callbackContext);
- break;
- case "get-available-encodings":
- getAvailableEncodings(callbackContext);
- break;
- case "decode":
- decode(arg1, arg2, callbackContext);
- break;
- case "encode":
- encode(arg1, arg2, callbackContext);
- break;
- case "compare-file-text":
- compareFileText(arg1, arg2, arg3, callbackContext);
- break;
- case "compare-texts":
- compareTexts(arg1, arg2, callbackContext);
- break;
- case "checksumText":
-
- cordova.getThreadPool().execute(() -> {
- try {
-
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
-
- byte[] hash = digest.digest(args.getString(0).getBytes("UTF-8"));
-
- StringBuilder hexString = new StringBuilder();
-
- for (byte b : hash) {
- String hex = Integer.toHexString(0xff & b);
-
- if (hex.length() == 1) hexString.append('0');
-
- hexString.append(hex);
- }
-
-
- callbackContext.success(hexString.toString());
- } catch (Exception e) {
- callbackContext.error(e.getMessage());
- }
- });
-
- break;
- default:
- break;
- }
- }
- }
- );
+ if (!Charset.isSupported(encoding)) {
+ callback.error("Charset not supported: " + encoding);
+ return;
+ }
- return true;
- }
+ Uri uri = Uri.parse(fileUri);
+ Charset charset = Charset.forName(encoding);
+ String fileContent;
- private void sendLogToJavaScript(String level, String message) {
- final String js =
- "if (typeof window.log === 'function')" +
- " window.log(" + JSONObject.quote(level) + ", " + JSONObject.quote(message) + ");" +
- "else" +
- " console.log(" + JSONObject.quote(level) + ", " + JSONObject.quote(message) + ");";
+ // Handle file:// URIs
+ if ("file".equalsIgnoreCase(uri.getScheme())) {
+ File file = new File(uri.getPath());
- cordova.getActivity().runOnUiThread(() -> {
+ // Validate file
+ if (!file.exists()) {
+ callback.error("File does not exist");
+ return;
+ }
+ if (!file.isFile()) {
+ callback.error("Path is not a file");
+ return;
+ }
+ if (!file.canRead()) {
+ callback.error("File is not readable");
+ return;
+ }
+
+ Path path = file.toPath();
+ fileContent = new String(Files.readAllBytes(path), charset);
+ } else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ // Handle content:// URIs (including SAF tree URIs)
+ InputStream inputStream = null;
+ try {
+ String uriString = fileUri;
+ Uri resolvedUri = uri;
+
+ // Check if this is a SAF tree URI with :: separator
+ if (uriString.contains("::")) {
try {
- ((android.webkit.WebView) webView.getEngine().getView())
- .evaluateJavascript(js, null);
+ // Split into tree URI and document ID
+ String[] parts = uriString.split("::", 2);
+ String treeUriStr = parts[0];
+ String docId = parts[1];
+
+ // Build document URI directly from tree URI and document ID
+ Uri treeUri = Uri.parse(treeUriStr);
+ resolvedUri = DocumentsContract.buildDocumentUriUsingTree(
+ treeUri,
+ docId
+ );
} catch (Exception e) {
- Log.e(TAG, "Failed to send log to JavaScript: " + e.getMessage());
+ callback.error(
+ "SAF_FALLBACK: Invalid SAF URI format - " + e.getMessage()
+ );
+ return;
}
- });
- }
-
- // Helper method to determine MIME type using Android's built-in MimeTypeMap
- private String getMimeTypeFromExtension(String fileName) {
- String extension = "";
- int lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
- extension = fileName.substring(lastDotIndex + 1).toLowerCase();
- }
+ }
- String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- return mimeType != null ? mimeType : "application/octet-stream";
- }
+ // Try to open the resolved URI
+ inputStream = context
+ .getContentResolver()
+ .openInputStream(resolvedUri);
- private void getConfiguration(CallbackContext callback) {
- try {
- JSONObject result = new JSONObject();
- Configuration config = context.getResources().getConfiguration();
- InputMethodManager imm = (InputMethodManager) context.getSystemService(
- Context.INPUT_METHOD_SERVICE
- );
- Method method =
- InputMethodManager.class.getMethod("getInputMethodWindowVisibleHeight");
-
- result.put("isAcceptingText", imm.isAcceptingText());
- result.put("keyboardHeight", method.invoke(imm));
- result.put("locale", config.locale.toString());
- result.put("fontScale", config.fontScale);
- result.put("keyboard", config.keyboard);
- result.put("keyboardHidden", config.keyboardHidden);
- result.put("hardKeyboardHidden", config.hardKeyboardHidden);
- result.put("navigationHidden", config.navigationHidden);
- result.put("navigation", config.navigation);
- result.put("orientation", config.orientation);
- callback.success(result);
- } catch (
- JSONException |
- NoSuchMethodException |
- IllegalAccessException |
- InvocationTargetException error
- ) {
- callback.error(error.toString());
+ if (inputStream == null) {
+ callback.error("Cannot open file");
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ try (
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(inputStream, charset)
+ )
+ ) {
+ char[] buffer = new char[8192];
+ int charsRead;
+ while ((charsRead = reader.read(buffer)) != -1) {
+ sb.append(buffer, 0, charsRead);
+ }
+ }
+ fileContent = sb.toString();
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException closeError) {
+ Log.w(
+ TAG,
+ "Failed to close input stream while reading file.",
+ closeError
+ );
+ }
+ }
}
- }
+ } else {
+ callback.error("Unsupported URI scheme: " + uri.getScheme());
+ return;
+ }
- private void decode(
- String content, // base64 encoded string
- String charSetName,
- CallbackContext callback
- ) {
- try {
- byte[] bytes = Base64.decode(content, Base64.DEFAULT);
+ // check length first
+ if (fileContent.length() != currentText.length()) {
+ callback.success(1); // Changed
+ return;
+ }
+
+ // Full comparison
+ if (fileContent.equals(currentText)) {
+ callback.success(0); // Not changed
+ } else {
+ callback.success(1); // Changed
+ }
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ /**
+ * Compares two text strings.
+ * This method runs in a background thread to avoid blocking the UI
+ * for large string comparisons.
+ *
+ * @param text1 First text to compare
+ * @param text2 Second text to compare
+ * @param callback Returns 1 if texts are different, 0 if same
+ */
+ private void compareTexts(
+ String text1,
+ String text2,
+ CallbackContext callback
+ ) {
+ try {
+ if (text1 == null) text1 = "";
+ if (text2 == null) text2 = "";
+
+ // check length first
+ if (text1.length() != text2.length()) {
+ callback.success(1); // Changed
+ return;
+ }
+
+ // Full comparison
+ if (text1.equals(text2)) {
+ callback.success(0); // Not changed
+ } else {
+ callback.success(1); // Changed
+ }
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void getAvailableEncodings(CallbackContext callback) {
+ try {
+ Map charsets = Charset.availableCharsets();
+ JSONObject result = new JSONObject();
+ for (Map.Entry entry : charsets.entrySet()) {
+ JSONObject obj = new JSONObject();
+ Charset charset = entry.getValue();
+ obj.put("label", charset.displayName());
+ obj.put("aliases", new JSONArray(charset.aliases()));
+ obj.put("name", charset.name());
+ result.put(charset.name(), obj);
+ }
+ callback.success(result);
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void requestPermissions(JSONArray arr, CallbackContext callback) {
+ try {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ int[] res = new int[arr.length()];
+ for (int i = 0; i < res.length; ++i) {
+ res[i] = 1;
+ }
+ callback.success(1);
+ return;
+ }
- if (Charset.isSupported(charSetName) == false) {
- callback.error("Charset not supported: " + charSetName);
- return;
- }
+ String[] permissions = checkPermissions(arr);
- Charset charSet = Charset.forName(charSetName);
- CharBuffer charBuffer = charSet.decode(ByteBuffer.wrap(bytes));
- String result = String.valueOf(charBuffer);
- callback.success(result);
- } catch (Exception e) {
- callback.error(e.toString());
- }
+ if (permissions.length > 0) {
+ requestPermissionCallback = callback;
+ cordova.requestPermissions(this, REQ_PERMISSIONS, permissions);
+ return;
+ }
+ callback.success(new JSONArray());
+ } catch (Exception e) {
+ callback.error(e.toString());
}
+ }
- private void encode(
- String content, // string to encode
- String charSetName,
- CallbackContext callback
- ) {
- try {
- if (Charset.isSupported(charSetName) == false) {
- callback.error("Charset not supported: " + charSetName);
- return;
- }
-
- Charset charSet = Charset.forName(charSetName);
- ByteBuffer byteBuffer = charSet.encode(content);
- byte[] bytes = new byte[byteBuffer.remaining()];
- byteBuffer.get(bytes);
- callback.success(bytes);
- } catch (Exception e) {
- callback.error(e.toString());
- }
+ private void requestPermission(String permission, CallbackContext callback) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ callback.success(1);
+ return;
}
- /**
- * Compares file content with provided text.
- * This method runs in a background thread to avoid blocking the UI.
- *
- * @param fileUri The URI of the file to read (file:// or content://)
- * @param encoding The character encoding to use when reading the file
- * @param currentText The text to compare against the file content
- * @param callback Returns 1 if texts are different, 0 if same
- */
- private void compareFileText(
- String fileUri,
- String encoding,
- String currentText,
- CallbackContext callback
+ if (permission != null || !permission.equals("")) {
+ if (!cordova.hasPermission(permission)) {
+ requestPermissionCallback = callback;
+ cordova.requestPermission(this, REQ_PERMISSION, permission);
+ return;
+ }
+
+ callback.success(1);
+ return;
+ }
+
+ callback.error("No permission passed to request.");
+ }
+
+ private void hasPermission(String permission, CallbackContext callback) {
+ if (permission != null || !permission.equals("")) {
+ int res = 0;
+ if (cordova.hasPermission(permission)) {
+ res = 1;
+ }
+
+ callback.success(res);
+ return;
+ }
+ callback.error("No permission passed to check.");
+ }
+
+ public boolean fileExists(String path, String countSymlinks) {
+ boolean countSymbolicLinks = Boolean.parseBoolean(countSymlinks);
+ File file = new File(path);
+
+ // Android < O does not implement File#toPath(), fall back to legacy checks
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return file.exists();
+ }
+
+ Path p = file.toPath();
+ try {
+ if (countSymbolicLinks) {
+ return Files.exists(p, LinkOption.NOFOLLOW_LINKS);
+ }
+ return Files.exists(p);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public boolean createSymlink(String target, String linkPath) {
+ try {
+ Process process = Runtime.getRuntime().exec(new String[] {
+ "ln",
+ "-s",
+ target,
+ linkPath,
+ });
+ return process.waitFor() == 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public String getNativeLibraryPath() {
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ return appInfo.nativeLibraryDir;
+ }
+
+ public String getFilesDir() {
+ return context.getFilesDir().getAbsolutePath();
+ }
+
+ public String getParentPath(String path) {
+ File file = new File(path);
+ File parent = file.getParentFile();
+ return parent != null ? parent.getAbsolutePath() : null;
+ }
+
+ public JSONArray listChildren(String path) throws JSONException {
+ File dir = new File(path);
+ JSONArray result = new JSONArray();
+ if (dir.exists() && dir.isDirectory()) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ result.put(file.getAbsolutePath());
+ }
+ }
+ }
+ return result;
+ }
+
+ public void onRequestPermissionResult(
+ int code,
+ String[] permissions,
+ int[] resCodes
+ ) {
+ if (requestPermissionCallback == null) return;
+
+ if (code == REQ_PERMISSIONS) {
+ JSONArray resAr = new JSONArray();
+ for (int res : resCodes) {
+ if (res == PackageManager.PERMISSION_DENIED) {
+ resAr.put(0);
+ }
+ resAr.put(1);
+ }
+
+ requestPermissionCallback.success(resAr);
+ requestPermissionCallback = null;
+ return;
+ }
+
+ if (
+ resCodes.length >= 1 && resCodes[0] == PackageManager.PERMISSION_DENIED
) {
+ requestPermissionCallback.success(0);
+ requestPermissionCallback = null;
+ return;
+ }
+ requestPermissionCallback.success(1);
+ requestPermissionCallback = null;
+ return;
+ }
+
+ private String[] checkPermissions(JSONArray arr) throws Exception {
+ List list = new ArrayList();
+ for (int i = 0; i < arr.length(); i++) {
+ try {
+ String permission = arr.getString(i);
+ if (permission == null || permission.equals("")) {
+ throw new Exception("Permission cannot be null or empty");
+ }
+ if (!cordova.hasPermission(permission)) {
+ list.add(permission);
+ }
+ } catch (JSONException e) {
+ Log.w(TAG, "Invalid permission entry at index " + i, e);
+ }
+ }
+
+ String[] res = new String[list.size()];
+ return list.toArray(res);
+ }
+
+ private void getAndroidVersion(CallbackContext callback) {
+ callback.success(Build.VERSION.SDK_INT);
+ }
+
+ private void getWebkitInfo(CallbackContext callback) {
+ PackageInfo info = null;
+ JSONObject res = new JSONObject();
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ info = WebView.getCurrentWebViewPackage();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Class webViewFactory = Class.forName("android.webkit.WebViewFactory");
+ Method method = webViewFactory.getMethod("getLoadedPackageInfo");
+ info = (PackageInfo) method.invoke(null);
+ } else {
+ PackageManager packageManager = activity.getPackageManager();
+
try {
- if (fileUri == null || fileUri.isEmpty()) {
- callback.error("File URI is required");
- return;
- }
-
- if (encoding == null || encoding.isEmpty()) {
- encoding = "UTF-8";
- }
-
- if (!Charset.isSupported(encoding)) {
- callback.error("Charset not supported: " + encoding);
- return;
- }
-
- Uri uri = Uri.parse(fileUri);
- Charset charset = Charset.forName(encoding);
- String fileContent;
-
- // Handle file:// URIs
- if ("file".equalsIgnoreCase(uri.getScheme())) {
- File file = new File(uri.getPath());
-
- // Validate file
- if (!file.exists()) {
- callback.error("File does not exist");
- return;
- }
- if (!file.isFile()) {
- callback.error("Path is not a file");
- return;
- }
- if (!file.canRead()) {
- callback.error("File is not readable");
- return;
- }
-
- Path path = file.toPath();
- fileContent = new String(Files.readAllBytes(path), charset);
-
- } else if ("content".equalsIgnoreCase(uri.getScheme())) {
- // Handle content:// URIs (including SAF tree URIs)
- InputStream inputStream = null;
- try {
- String uriString = fileUri;
- Uri resolvedUri = uri;
-
- // Check if this is a SAF tree URI with :: separator
- if (uriString.contains("::")) {
- try {
- // Split into tree URI and document ID
- String[] parts = uriString.split("::", 2);
- String treeUriStr = parts[0];
- String docId = parts[1];
-
- // Build document URI directly from tree URI and document ID
- Uri treeUri = Uri.parse(treeUriStr);
- resolvedUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId);
- } catch (Exception e) {
- callback.error("SAF_FALLBACK: Invalid SAF URI format - " + e.getMessage());
- return;
- }
- }
-
- // Try to open the resolved URI
- inputStream = context.getContentResolver().openInputStream(resolvedUri);
-
- if (inputStream == null) {
- callback.error("Cannot open file");
- return;
- }
-
- StringBuilder sb = new StringBuilder();
- try (BufferedReader reader = new BufferedReader(
- new InputStreamReader(inputStream, charset))) {
- char[] buffer = new char[8192];
- int charsRead;
- while ((charsRead = reader.read(buffer)) != -1) {
- sb.append(buffer, 0, charsRead);
- }
- }
- fileContent = sb.toString();
-
- } finally {
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (IOException closeError) {
- Log.w(TAG, "Failed to close input stream while reading file.", closeError);
- }
- }
- }
- } else {
- callback.error("Unsupported URI scheme: " + uri.getScheme());
- return;
- }
-
- // check length first
- if (fileContent.length() != currentText.length()) {
- callback.success(1); // Changed
- return;
- }
-
- // Full comparison
- if (fileContent.equals(currentText)) {
- callback.success(0); // Not changed
- } else {
- callback.success(1); // Changed
- }
-
- } catch (Exception e) {
- callback.error(e.toString());
+ info = packageManager.getPackageInfo("com.google.android.webview", 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ callback.error("Package not found");
}
- }
- /**
- * Compares two text strings.
- * This method runs in a background thread to avoid blocking the UI
- * for large string comparisons.
- *
- * @param text1 First text to compare
- * @param text2 Second text to compare
- * @param callback Returns 1 if texts are different, 0 if same
- */
- private void compareTexts(
- String text1,
- String text2,
- CallbackContext callback
+ return;
+ }
+
+ res.put("packageName", info.packageName);
+ res.put("versionName", info.versionName);
+ res.put("versionCode", info.versionCode);
+
+ callback.success(res);
+ } catch (
+ JSONException
+ | InvocationTargetException
+ | ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalAccessException e
) {
- try {
- if (text1 == null) text1 = "";
- if (text2 == null) text2 = "";
-
- // check length first
- if (text1.length() != text2.length()) {
- callback.success(1); // Changed
- return;
- }
-
- // Full comparison
- if (text1.equals(text2)) {
- callback.success(0); // Not changed
- } else {
- callback.success(1); // Changed
- }
-
- } catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private void getAvailableEncodings(CallbackContext callback) {
- try {
- Map < String, Charset > charsets = Charset.availableCharsets();
- JSONObject result = new JSONObject();
- for (Map.Entry < String, Charset > entry: charsets.entrySet()) {
- JSONObject obj = new JSONObject();
- Charset charset = entry.getValue();
- obj.put("label", charset.displayName());
- obj.put("aliases", new JSONArray(charset.aliases()));
- obj.put("name", charset.name());
- result.put(charset.name(), obj);
- }
- callback.success(result);
- } catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private void requestPermissions(JSONArray arr, CallbackContext callback) {
- try {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- int[] res = new int[arr.length()];
- for (int i = 0; i < res.length; ++i) {
- res[i] = 1;
- }
- callback.success(1);
- return;
- }
+ callback.error(
+ "Cannot determine current WebView engine. (" + e.getMessage() + ")"
+ );
- String[] permissions = checkPermissions(arr);
-
- if (permissions.length > 0) {
- requestPermissionCallback = callback;
- cordova.requestPermissions(this, REQ_PERMISSIONS, permissions);
- return;
- }
- callback.success(new JSONArray());
- } catch (Exception e) {
- callback.error(e.toString());
- }
+ return;
}
+ }
- private void requestPermission(String permission, CallbackContext callback) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- callback.success(1);
- return;
- }
-
- if (permission != null || !permission.equals("")) {
- if (!cordova.hasPermission(permission)) {
- requestPermissionCallback = callback;
- cordova.requestPermission(this, REQ_PERMISSION, permission);
- return;
- }
+ private void isPowerSaveMode(CallbackContext callback) {
+ PowerManager powerManager = (PowerManager) context.getSystemService(
+ Context.POWER_SERVICE
+ );
+ boolean powerSaveMode = powerManager.isPowerSaveMode();
- callback.success(1);
- return;
- }
+ callback.success(powerSaveMode ? 1 : 0);
+ }
- callback.error("No permission passed to request.");
+ private void pinFileShortcut(
+ JSONObject shortcutJson,
+ CallbackContext callback
+ ) {
+ if (shortcutJson == null) {
+ callback.error("Invalid shortcut data");
+ return;
}
- private void hasPermission(String permission, CallbackContext callback) {
- if (permission != null || !permission.equals("")) {
- int res = 0;
- if (cordova.hasPermission(permission)) {
- res = 1;
- }
+ String id = shortcutJson.optString("id", "");
+ String label = shortcutJson.optString("label", "");
+ String description = shortcutJson.optString("description", label);
+ String iconSrc = shortcutJson.optString("icon", "");
+ String uriString = shortcutJson.optString("uri", "");
- callback.success(res);
- return;
- }
- callback.error("No permission passed to check.");
+ if (id.isEmpty() || label.isEmpty() || uriString.isEmpty()) {
+ callback.error("Missing required shortcut fields");
+ return;
}
- public boolean fileExists(String path, String countSymlinks) {
- boolean countSymbolicLinks = Boolean.parseBoolean(countSymlinks);
- File file = new File(path);
-
- // Android < O does not implement File#toPath(), fall back to legacy checks
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- return file.exists();
- }
-
- Path p = file.toPath();
- try {
- if (countSymbolicLinks) {
- return Files.exists(p, LinkOption.NOFOLLOW_LINKS);
- }
- return Files.exists(p);
- } catch (Exception e) {
- return false;
- }
+ if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ callback.error("Pin shortcut not supported on this launcher");
+ return;
}
- public boolean createSymlink(String target, String linkPath) {
- try {
- Process process = Runtime.getRuntime().exec(new String[] {
- "ln",
- "-s",
- target,
- linkPath
- });
- return process.waitFor() == 0;
- } catch (Exception e) {
- return false;
- }
- }
-
- public String getNativeLibraryPath() {
- ApplicationInfo appInfo = context.getApplicationInfo();
- return appInfo.nativeLibraryDir;
- }
-
- public String getFilesDir() {
- return context.getFilesDir().getAbsolutePath();
- }
-
- public String getParentPath(String path) {
- File file = new File(path);
- File parent = file.getParentFile();
- return parent != null ? parent.getAbsolutePath() : null;
- }
-
- public JSONArray listChildren(String path) throws JSONException {
- File dir = new File(path);
- JSONArray result = new JSONArray();
- if (dir.exists() && dir.isDirectory()) {
- File[] files = dir.listFiles();
- if (files != null) {
- for (File file: files) {
- result.put(file.getAbsolutePath());
- }
- }
- }
- return result;
- }
+ try {
+ Uri dataUri = Uri.parse(uriString);
+ String packageName = context.getPackageName();
+ PackageManager pm = context.getPackageManager();
- public void onRequestPermissionResult(
- int code,
- String[] permissions,
- int[] resCodes
- ) {
- if (requestPermissionCallback == null) return;
-
- if (code == REQ_PERMISSIONS) {
- JSONArray resAr = new JSONArray();
- for (int res: resCodes) {
- if (res == PackageManager.PERMISSION_DENIED) {
- resAr.put(0);
- }
- resAr.put(1);
- }
-
- requestPermissionCallback.success(resAr);
- requestPermissionCallback = null;
- return;
- }
-
- if (
- resCodes.length >= 1 && resCodes[0] == PackageManager.PERMISSION_DENIED
- ) {
- requestPermissionCallback.success(0);
- requestPermissionCallback = null;
- return;
- }
- requestPermissionCallback.success(1);
- requestPermissionCallback = null;
+ Intent launchIntent = pm.getLaunchIntentForPackage(packageName);
+ if (launchIntent == null) {
+ callback.error("Launch intent not found for package: " + packageName);
return;
- }
-
- private String[] checkPermissions(JSONArray arr) throws Exception {
- List < String > list = new ArrayList < String > ();
- for (int i = 0; i < arr.length(); i++) {
- try {
- String permission = arr.getString(i);
- if (permission == null || permission.equals("")) {
- throw new Exception("Permission cannot be null or empty");
- }
- if (!cordova.hasPermission(permission)) {
- list.add(permission);
- }
- } catch (JSONException e) {
- Log.w(TAG, "Invalid permission entry at index " + i, e);
- }
- }
+ }
- String[] res = new String[list.size()];
- return list.toArray(res);
- }
+ ComponentName componentName = launchIntent.getComponent();
+ if (componentName == null) {
+ callback.error("ComponentName is null");
+ return;
+ }
- private void getAndroidVersion(CallbackContext callback) {
- callback.success(Build.VERSION.SDK_INT);
- }
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setComponent(componentName);
+ intent.setData(dataUri);
+ intent.putExtra("acodeFileUri", uriString);
- private void getWebkitInfo(CallbackContext callback) {
- PackageInfo info = null;
- JSONObject res = new JSONObject();
+ IconCompat icon;
+ if (iconSrc != null && !iconSrc.isEmpty()) {
try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- info = WebView.getCurrentWebViewPackage();
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Class webViewFactory = Class.forName("android.webkit.WebViewFactory");
- Method method = webViewFactory.getMethod("getLoadedPackageInfo");
- info = (PackageInfo) method.invoke(null);
- } else {
- PackageManager packageManager = activity.getPackageManager();
-
- try {
- info = packageManager.getPackageInfo("com.google.android.webview", 0);
- } catch (PackageManager.NameNotFoundException e) {
- callback.error("Package not found");
- }
-
- return;
- }
-
- res.put("packageName", info.packageName);
- res.put("versionName", info.versionName);
- res.put("versionCode", info.versionCode);
-
- callback.success(res);
- } catch (
- JSONException |
- InvocationTargetException |
- ClassNotFoundException |
- NoSuchMethodException |
- IllegalAccessException e
- ) {
- callback.error(
- "Cannot determine current WebView engine. (" + e.getMessage() + ")"
+ Uri iconUri = Uri.parse(iconSrc);
+ Bitmap bitmap;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ // API 28+
+ ImageDecoder.Source source = ImageDecoder.createSource(
+ context.getContentResolver(),
+ iconUri
);
-
- return;
- }
- }
-
- private void isPowerSaveMode(CallbackContext callback) {
- PowerManager powerManager = (PowerManager) context.getSystemService(
- Context.POWER_SERVICE
- );
- boolean powerSaveMode = powerManager.isPowerSaveMode();
-
- callback.success(powerSaveMode ? 1 : 0);
- }
-
- private void pinFileShortcut(JSONObject shortcutJson, CallbackContext callback) {
- if (shortcutJson == null) {
- callback.error("Invalid shortcut data");
- return;
- }
-
- String id = shortcutJson.optString("id", "");
- String label = shortcutJson.optString("label", "");
- String description = shortcutJson.optString("description", label);
- String iconSrc = shortcutJson.optString("icon", "");
- String uriString = shortcutJson.optString("uri", "");
-
- if (id.isEmpty() || label.isEmpty() || uriString.isEmpty()) {
- callback.error("Missing required shortcut fields");
- return;
- }
-
- if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
- callback.error("Pin shortcut not supported on this launcher");
- return;
- }
-
- try {
- Uri dataUri = Uri.parse(uriString);
- String packageName = context.getPackageName();
- PackageManager pm = context.getPackageManager();
-
- Intent launchIntent = pm.getLaunchIntentForPackage(packageName);
- if (launchIntent == null) {
- callback.error("Launch intent not found for package: " + packageName);
- return;
- }
-
- ComponentName componentName = launchIntent.getComponent();
- if (componentName == null) {
- callback.error("ComponentName is null");
- return;
- }
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setComponent(componentName);
- intent.setData(dataUri);
- intent.putExtra("acodeFileUri", uriString);
-
- IconCompat icon;
-
- if (iconSrc != null && !iconSrc.isEmpty()) {
- try {
- Uri iconUri = Uri.parse(iconSrc);
- Bitmap bitmap;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- // API 28+
- ImageDecoder.Source source =
- ImageDecoder.createSource(
- context.getContentResolver(),
- iconUri
- );
- bitmap = ImageDecoder.decodeBitmap(source);
- } else {
- // Below API 28
- bitmap = MediaStore.Images.Media.getBitmap(
- context.getContentResolver(),
- iconUri
- );
- }
-
- icon = IconCompat.createWithBitmap(bitmap);
-
- } catch (Exception e) {
- icon = getFileShortcutIcon(label);
- }
- } else {
- icon = getFileShortcutIcon(label);
- }
-
- ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id)
- .setShortLabel(label)
- .setLongLabel(
- description != null && !description.isEmpty() ? description : label
- )
- .setIcon(icon)
- .setIntent(intent)
- .build();
-
- ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
-
- boolean requested = ShortcutManagerCompat.requestPinShortcut(
- context,
- shortcut,
- null
+ bitmap = ImageDecoder.decodeBitmap(source);
+ } else {
+ // Below API 28
+ bitmap = MediaStore.Images.Media.getBitmap(
+ context.getContentResolver(),
+ iconUri
);
+ }
- if (!requested) {
- callback.error("Failed to request pin shortcut");
- return;
- }
-
- callback.success();
+ icon = IconCompat.createWithBitmap(bitmap);
} catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private IconCompat getFileShortcutIcon(String filename) {
- Bitmap fallback = createFileShortcutBitmap(filename);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- return IconCompat.createWithAdaptiveBitmap(fallback);
- }
- return IconCompat.createWithBitmap(fallback);
- }
-
- private Bitmap createFileShortcutBitmap(String filename) {
- final float baseSizeDp = 72f;
- float sizePx = TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- baseSizeDp,
- context.getResources().getDisplayMetrics()
+ icon = getFileShortcutIcon(label);
+ }
+ } else {
+ icon = getFileShortcutIcon(label);
+ }
+
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id)
+ .setShortLabel(label)
+ .setLongLabel(
+ description != null && !description.isEmpty() ? description : label
+ )
+ .setIcon(icon)
+ .setIntent(intent)
+ .build();
+
+ ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
+
+ boolean requested = ShortcutManagerCompat.requestPinShortcut(
+ context,
+ shortcut,
+ null
+ );
+
+ if (!requested) {
+ callback.error("Failed to request pin shortcut");
+ return;
+ }
+
+ callback.success();
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private IconCompat getFileShortcutIcon(String filename) {
+ Bitmap fallback = createFileShortcutBitmap(filename);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return IconCompat.createWithAdaptiveBitmap(fallback);
+ }
+ return IconCompat.createWithBitmap(fallback);
+ }
+
+ private Bitmap createFileShortcutBitmap(String filename) {
+ final float baseSizeDp = 72f;
+ float sizePx = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ baseSizeDp,
+ context.getResources().getDisplayMetrics()
+ );
+ if (sizePx <= 0) {
+ sizePx = baseSizeDp;
+ }
+ int size = Math.round(sizePx);
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint(
+ Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG
+ );
+
+ int backgroundColor = pickShortcutColor(filename);
+ paint.setColor(backgroundColor);
+ float radius = size * 0.24f;
+ RectF bounds = new RectF(0, 0, size, size);
+ canvas.drawRoundRect(bounds, radius, radius, paint);
+
+ paint.setColor(Color.WHITE);
+ paint.setTextAlign(Paint.Align.CENTER);
+ paint.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD));
+
+ String label = getShortcutLabel(filename);
+ float textLength = Math.max(1, label.length());
+ float factor = textLength > 4 ? 0.22f : textLength > 3 ? 0.26f : 0.34f;
+ paint.setTextSize(size * factor);
+ Paint.FontMetrics metrics = paint.getFontMetrics();
+ float baseline = (size - metrics.bottom - metrics.top) / 2f;
+ canvas.drawText(label, size / 2f, baseline, paint);
+
+ return bitmap;
+ }
+
+ private String getFileExtension(String filename) {
+ if (filename == null) return "";
+ int dot = filename.lastIndexOf('.');
+ if (dot < 0 || dot == filename.length() - 1) return "";
+ return filename.substring(dot + 1).toLowerCase(Locale.getDefault());
+ }
+
+ private String getShortcutLabel(String filename) {
+ String ext = getFileExtension(filename);
+ if (!ext.isEmpty()) {
+ switch (ext) {
+ case "js":
+ case "jsx":
+ return "JS";
+ case "ts":
+ case "tsx":
+ return "TS";
+ case "md":
+ case "markdown":
+ return "MD";
+ case "json":
+ return "JSON";
+ case "html":
+ case "htm":
+ return "HTML";
+ case "css":
+ return "CSS";
+ case "java":
+ return "JAVA";
+ case "kt":
+ case "kts":
+ return "KOT";
+ case "py":
+ return "PY";
+ case "rb":
+ return "RB";
+ case "c":
+ return "C";
+ case "cpp":
+ case "cc":
+ case "cxx":
+ return "CPP";
+ case "h":
+ case "hpp":
+ return "HDR";
+ case "go":
+ return "GO";
+ case "rs":
+ return "RS";
+ case "php":
+ return "PHP";
+ case "xml":
+ return "XML";
+ case "yml":
+ case "yaml":
+ return "YML";
+ case "txt":
+ return "TXT";
+ case "sh":
+ case "bash":
+ return "SH";
+ default:
+ String label = ext.replaceAll("[^A-Za-z0-9]", "");
+ if (label.isEmpty()) label = ext;
+ if (label.length() > 4) {
+ label = label.substring(0, 4);
+ }
+ return label.toUpperCase(Locale.getDefault());
+ }
+ }
+
+ if (filename != null && !filename.trim().isEmpty()) {
+ String cleaned = filename.replaceAll("[^A-Za-z0-9]", "");
+ if (!cleaned.isEmpty()) {
+ if (cleaned.length() > 3) cleaned = cleaned.substring(0, 3);
+ return cleaned.toUpperCase(Locale.getDefault());
+ }
+ return filename.substring(0, 1).toUpperCase(Locale.getDefault());
+ }
+
+ return "FILE";
+ }
+
+ private int pickShortcutColor(String filename) {
+ String ext = getFileExtension(filename);
+ switch (ext) {
+ case "js":
+ case "jsx":
+ return 0xFFF7DF1E;
+ case "ts":
+ case "tsx":
+ return 0xFF3178C6;
+ case "md":
+ case "markdown":
+ return 0xFF546E7A;
+ case "json":
+ return 0xFF4CAF50;
+ case "html":
+ case "htm":
+ return 0xFFF4511E;
+ case "css":
+ return 0xFF2962FF;
+ case "java":
+ return 0xFFEC6F2D;
+ case "kt":
+ case "kts":
+ return 0xFF7F52FF;
+ case "py":
+ return 0xFF306998;
+ case "rb":
+ return 0xFFCC342D;
+ case "c":
+ return 0xFF546E7A;
+ case "cpp":
+ case "cc":
+ case "cxx":
+ return 0xFF00599C;
+ case "h":
+ case "hpp":
+ return 0xFF8D6E63;
+ case "go":
+ return 0xFF00ADD8;
+ case "rs":
+ return 0xFFB7410E;
+ case "php":
+ return 0xFF8892BF;
+ case "xml":
+ return 0xFF5C6BC0;
+ case "yml":
+ case "yaml":
+ return 0xFF757575;
+ case "txt":
+ return 0xFF546E7A;
+ case "sh":
+ case "bash":
+ return 0xFF388E3C;
+ default:
+ final int[] colors = new int[] {
+ 0xFF1E88E5,
+ 0xFF6D4C41,
+ 0xFF00897B,
+ 0xFF8E24AA,
+ 0xFF3949AB,
+ 0xFF039BE5,
+ 0xFFD81B60,
+ 0xFF43A047,
+ };
+ String key = ext.isEmpty()
+ ? (filename == null ? "file" : filename)
+ : ext;
+ int hash = Math.abs(key.hashCode());
+ return colors[hash % colors.length];
+ }
+ }
+
+ private void fileAction(
+ String fileURI,
+ String filename,
+ String action,
+ String mimeType,
+ CallbackContext callback
+ ) {
+ Activity activity = this.activity;
+ Context context = this.context;
+ Uri uri = this.getContentProviderUri(fileURI, filename);
+ if (uri == null) {
+ callback.error("Unable to access file for action " + action);
+ return;
+ }
+ try {
+ Intent intent = new Intent(action);
+
+ if (mimeType.equals("")) {
+ mimeType = "text/plain";
+ }
+
+ mimeType = resolveMimeType(mimeType, uri, filename);
+
+ String clipLabel = null;
+ if (filename != null && !filename.isEmpty()) {
+ clipLabel = new File(filename).getName();
+ }
+ if (clipLabel == null || clipLabel.isEmpty()) {
+ clipLabel = uri.getLastPathSegment();
+ }
+ if (clipLabel == null || clipLabel.isEmpty()) {
+ clipLabel = "shared-file";
+ }
+ if (action.equals(Intent.ACTION_SEND)) {
+ intent.setType(mimeType);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setClipData(
+ ClipData.newUri(context.getContentResolver(), clipLabel, uri)
);
- if (sizePx <= 0) {
- sizePx = baseSizeDp;
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
+ intent.putExtra(Intent.EXTRA_SUBJECT, clipLabel);
+ if (filename != null && !filename.isEmpty()) {
+ intent.putExtra(Intent.EXTRA_TEXT, filename);
}
- int size = Math.round(sizePx);
- Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG);
-
- int backgroundColor = pickShortcutColor(filename);
- paint.setColor(backgroundColor);
- float radius = size * 0.24f;
- RectF bounds = new RectF(0, 0, size, size);
- canvas.drawRoundRect(bounds, radius, radius, paint);
-
- paint.setColor(Color.WHITE);
- paint.setTextAlign(Paint.Align.CENTER);
- paint.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD));
-
- String label = getShortcutLabel(filename);
- float textLength = Math.max(1, label.length());
- float factor = textLength > 4 ? 0.22f : textLength > 3 ? 0.26f : 0.34f;
- paint.setTextSize(size * factor);
- Paint.FontMetrics metrics = paint.getFontMetrics();
- float baseline = (size - metrics.bottom - metrics.top) / 2f;
- canvas.drawText(label, size / 2f, baseline, paint);
-
- return bitmap;
- }
-
- private String getFileExtension(String filename) {
- if (filename == null) return "";
- int dot = filename.lastIndexOf('.');
- if (dot < 0 || dot == filename.length() - 1) return "";
- return filename.substring(dot + 1).toLowerCase(Locale.getDefault());
- }
-
- private String getShortcutLabel(String filename) {
- String ext = getFileExtension(filename);
- if (!ext.isEmpty()) {
- switch (ext) {
- case "js":
- case "jsx":
- return "JS";
- case "ts":
- case "tsx":
- return "TS";
- case "md":
- case "markdown":
- return "MD";
- case "json":
- return "JSON";
- case "html":
- case "htm":
- return "HTML";
- case "css":
- return "CSS";
- case "java":
- return "JAVA";
- case "kt":
- case "kts":
- return "KOT";
- case "py":
- return "PY";
- case "rb":
- return "RB";
- case "c":
- return "C";
- case "cpp":
- case "cc":
- case "cxx":
- return "CPP";
- case "h":
- case "hpp":
- return "HDR";
- case "go":
- return "GO";
- case "rs":
- return "RS";
- case "php":
- return "PHP";
- case "xml":
- return "XML";
- case "yml":
- case "yaml":
- return "YML";
- case "txt":
- return "TXT";
- case "sh":
- case "bash":
- return "SH";
- default:
- String label = ext.replaceAll("[^A-Za-z0-9]", "");
- if (label.isEmpty()) label = ext;
- if (label.length() > 4) {
- label = label.substring(0, 4);
- }
- return label.toUpperCase(Locale.getDefault());
- }
- }
-
- if (filename != null && !filename.trim().isEmpty()) {
- String cleaned = filename.replaceAll("[^A-Za-z0-9]", "");
- if (!cleaned.isEmpty()) {
- if (cleaned.length() > 3) cleaned = cleaned.substring(0, 3);
- return cleaned.toUpperCase(Locale.getDefault());
- }
- return filename.substring(0, 1).toUpperCase(Locale.getDefault());
- }
-
- return "FILE";
- }
-
- private int pickShortcutColor(String filename) {
- String ext = getFileExtension(filename);
- switch (ext) {
- case "js":
- case "jsx":
- return 0xFFF7DF1E;
- case "ts":
- case "tsx":
- return 0xFF3178C6;
- case "md":
- case "markdown":
- return 0xFF546E7A;
- case "json":
- return 0xFF4CAF50;
- case "html":
- case "htm":
- return 0xFFF4511E;
- case "css":
- return 0xFF2962FF;
- case "java":
- return 0xFFEC6F2D;
- case "kt":
- case "kts":
- return 0xFF7F52FF;
- case "py":
- return 0xFF306998;
- case "rb":
- return 0xFFCC342D;
- case "c":
- return 0xFF546E7A;
- case "cpp":
- case "cc":
- case "cxx":
- return 0xFF00599C;
- case "h":
- case "hpp":
- return 0xFF8D6E63;
- case "go":
- return 0xFF00ADD8;
- case "rs":
- return 0xFFB7410E;
- case "php":
- return 0xFF8892BF;
- case "xml":
- return 0xFF5C6BC0;
- case "yml":
- case "yaml":
- return 0xFF757575;
- case "txt":
- return 0xFF546E7A;
- case "sh":
- case "bash":
- return 0xFF388E3C;
- default:
- final int[] colors = new int[] {
- 0xFF1E88E5,
- 0xFF6D4C41,
- 0xFF00897B,
- 0xFF8E24AA,
- 0xFF3949AB,
- 0xFF039BE5,
- 0xFFD81B60,
- 0xFF43A047
- };
- String key = ext.isEmpty()
- ? (filename == null ? "file" : filename)
- : ext;
- int hash = Math.abs(key.hashCode());
- return colors[hash % colors.length];
- }
- }
+ } else {
+ int flags =
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
+ Intent.FLAG_GRANT_READ_URI_PERMISSION;
- private void fileAction(
- String fileURI,
- String filename,
- String action,
- String mimeType,
- CallbackContext callback
- ) {
- Activity activity = this.activity;
- Context context = this.context;
- Uri uri = this.getContentProviderUri(fileURI, filename);
- if (uri == null) {
- callback.error("Unable to access file for action " + action);
- return;
+ if (action.equals(Intent.ACTION_EDIT)) {
+ flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
}
- try {
- Intent intent = new Intent(action);
- if (mimeType.equals("")) {
- mimeType = "text/plain";
- }
-
- mimeType = resolveMimeType(mimeType, uri, filename);
-
- String clipLabel = null;
- if (filename != null && !filename.isEmpty()) {
- clipLabel = new File(filename).getName();
- }
- if (clipLabel == null || clipLabel.isEmpty()) {
- clipLabel = uri.getLastPathSegment();
- }
- if (clipLabel == null || clipLabel.isEmpty()) {
- clipLabel = "shared-file";
- }
- if (action.equals(Intent.ACTION_SEND)) {
- intent.setType(mimeType);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.setClipData(
- ClipData.newUri(
- context.getContentResolver(),
- clipLabel,
- uri
- )
- );
- intent.putExtra(Intent.EXTRA_STREAM, uri);
- intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
- intent.putExtra(Intent.EXTRA_SUBJECT, clipLabel);
- if (filename != null && !filename.isEmpty()) {
- intent.putExtra(Intent.EXTRA_TEXT, filename);
- }
- } else {
- int flags =
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
- Intent.FLAG_GRANT_READ_URI_PERMISSION;
-
- if (action.equals(Intent.ACTION_EDIT)) {
- flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
- }
-
- intent.setFlags(flags);
- intent.setDataAndType(uri, mimeType);
- intent.setClipData(
- ClipData.newUri(
- context.getContentResolver(),
- clipLabel,
- uri
- )
- );
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- if (!clipLabel.equals("shared-file")) {
- intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
- }
- if (action.equals(Intent.ACTION_EDIT)) {
- intent.putExtra(Intent.EXTRA_STREAM, uri);
- }
- }
-
- int permissionFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
- if (action.equals(Intent.ACTION_EDIT)) {
- permissionFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
- }
- grantUriPermissions(intent, uri, permissionFlags);
-
- if (action.equals(Intent.ACTION_SEND)) {
- Intent chooserIntent = Intent.createChooser(intent, null);
- chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- activity.startActivity(chooserIntent);
- } else if (action.equals(Intent.ACTION_EDIT) || action.equals(Intent.ACTION_VIEW)) {
- Intent chooserIntent = Intent.createChooser(intent, null);
- chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- if (action.equals(Intent.ACTION_EDIT)) {
- chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
- activity.startActivity(chooserIntent);
- } else {
- activity.startActivity(intent);
- }
- callback.success(uri.toString());
- } catch (Exception e) {
- callback.error(e.getMessage());
- }
- }
-
- private void getAppInfo(CallbackContext callback) {
- JSONObject res = new JSONObject();
- try {
- PackageManager pm = activity.getPackageManager();
- PackageInfo pInfo = pm.getPackageInfo(context.getPackageName(), 0);
- ApplicationInfo appInfo = context.getApplicationInfo();
- int isDebuggable = appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE;
-
- res.put("firstInstallTime", pInfo.firstInstallTime);
- res.put("lastUpdateTime", pInfo.lastUpdateTime);
- res.put("label", appInfo.loadLabel(pm).toString());
- res.put("packageName", pInfo.packageName);
- res.put("versionName", pInfo.versionName);
- res.put("versionCode", pInfo.getLongVersionCode());
- res.put("isDebuggable", isDebuggable);
-
- callback.success(res);
- } catch (JSONException e) {
- callback.error(e.getMessage());
- } catch (Exception e) {
- callback.error(e.getMessage());
- }
- }
-
- private void openInBrowser(String src, CallbackContext callback) {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(src));
- activity.startActivity(browserIntent);
- }
-
- private void launchApp(
- String appId,
- String className,
- JSONObject extras,
- CallbackContext callback
- ) {
- if (appId == null || appId.equals("")) {
- callback.error("No package name provided.");
- return;
- }
-
- if (className == null || className.equals("")) {
- callback.error("No activity class name provided.");
- return;
- }
-
- try {
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- intent.setPackage(appId);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setClassName(appId, className);
-
- if (extras != null) {
- Iterator keys = extras.keys();
-
- while (keys.hasNext()) {
- String key = keys.next();
- Object value = extras.get(key);
-
- if (value instanceof Integer) {
- intent.putExtra(key, (Integer) value);
- } else if (value instanceof Boolean) {
- intent.putExtra(key, (Boolean) value);
- } else if (value instanceof Double) {
- intent.putExtra(key, (Double) value);
- } else if (value instanceof Long) {
- intent.putExtra(key, (Long) value);
- } else if (value instanceof String) {
- intent.putExtra(key, (String) value);
- } else {
- intent.putExtra(key, value.toString());
- }
- }
- }
-
- activity.startActivity(intent);
- callback.success("Launched " + appId);
-
- } catch (Exception e) {
- callback.error(e.toString());
- }
-
- }
-
-
- private void addShortcut(
- String id,
- String label,
- String description,
- String iconSrc,
- String action,
- String data,
- CallbackContext callback
- ) {
- try {
- Intent intent;
- ImageDecoder.Source imgSrc;
- Bitmap bitmap;
- IconCompat icon;
-
- imgSrc = ImageDecoder.createSource(
- context.getContentResolver(),
- Uri.parse(iconSrc)
- );
- bitmap = ImageDecoder.decodeBitmap(imgSrc);
- icon = IconCompat.createWithBitmap(bitmap);
- intent = activity
- .getPackageManager()
- .getLaunchIntentForPackage(activity.getPackageName());
- intent.putExtra("action", action);
- intent.putExtra("data", data);
-
- ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id)
- .setShortLabel(label)
- .setLongLabel(description)
- .setIcon(icon)
- .setIntent(intent)
- .build();
-
- ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
- callback.success();
- } catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private void pinShortcut(String id, CallbackContext callback) {
- ShortcutManager shortcutManager = context.getSystemService(
- ShortcutManager.class
+ intent.setFlags(flags);
+ intent.setDataAndType(uri, mimeType);
+ intent.setClipData(
+ ClipData.newUri(context.getContentResolver(), clipLabel, uri)
);
-
- if (shortcutManager.isRequestPinShortcutSupported()) {
- ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(
- context,
- id
- ).build();
-
- Intent pinnedShortcutCallbackIntent =
- shortcutManager.createShortcutResultIntent(pinShortcutInfo);
-
- PendingIntent successCallback = PendingIntent.getBroadcast(
- context,
- 0,
- pinnedShortcutCallbackIntent,
- PendingIntent.FLAG_IMMUTABLE
- );
-
- shortcutManager.requestPinShortcut(
- pinShortcutInfo,
- successCallback.getIntentSender()
- );
-
- callback.success();
- return;
- }
-
- callback.error("Not supported");
- }
-
- private void removeShortcut(String id, CallbackContext callback) {
- try {
- List < String > list = new ArrayList < String > ();
- list.add(id);
- ShortcutManagerCompat.removeDynamicShortcuts(context, list);
- callback.success();
- } catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private void setUiTheme(
- final String systemBarColor,
- final JSONObject scheme,
- final CallbackContext callback
- ) {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (!clipLabel.equals("shared-file")) {
+ intent.putExtra(Intent.EXTRA_TITLE, clipLabel);
+ }
+ if (action.equals(Intent.ACTION_EDIT)) {
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ }
+ }
+
+ int permissionFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+ if (action.equals(Intent.ACTION_EDIT)) {
+ permissionFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+ }
+ grantUriPermissions(intent, uri, permissionFlags);
+
+ if (action.equals(Intent.ACTION_SEND)) {
+ Intent chooserIntent = Intent.createChooser(intent, null);
+ chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.startActivity(chooserIntent);
+ } else if (
+ action.equals(Intent.ACTION_EDIT) || action.equals(Intent.ACTION_VIEW)
+ ) {
+ Intent chooserIntent = Intent.createChooser(intent, null);
+ chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (action.equals(Intent.ACTION_EDIT)) {
+ chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+ activity.startActivity(chooserIntent);
+ } else {
+ activity.startActivity(intent);
+ }
+ callback.success(uri.toString());
+ } catch (Exception e) {
+ callback.error(e.getMessage());
+ }
+ }
+
+ private void getAppInfo(CallbackContext callback) {
+ JSONObject res = new JSONObject();
+ try {
+ PackageManager pm = activity.getPackageManager();
+ PackageInfo pInfo = pm.getPackageInfo(context.getPackageName(), 0);
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ int isDebuggable = appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE;
+
+ res.put("firstInstallTime", pInfo.firstInstallTime);
+ res.put("lastUpdateTime", pInfo.lastUpdateTime);
+ res.put("label", appInfo.loadLabel(pm).toString());
+ res.put("packageName", pInfo.packageName);
+ res.put("versionName", pInfo.versionName);
+ res.put("versionCode", pInfo.getLongVersionCode());
+ res.put("isDebuggable", isDebuggable);
+
+ callback.success(res);
+ } catch (JSONException e) {
+ callback.error(e.getMessage());
+ } catch (Exception e) {
+ callback.error(e.getMessage());
+ }
+ }
+
+ private void openInBrowser(String src, CallbackContext callback) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(src));
+ activity.startActivity(browserIntent);
+ }
+
+ private void launchApp(
+ String appId,
+ String className,
+ JSONObject extras,
+ CallbackContext callback
+ ) {
+ if (appId == null || appId.equals("")) {
+ callback.error("No package name provided.");
+ return;
+ }
+
+ if (className == null || className.equals("")) {
+ callback.error("No activity class name provided.");
+ return;
+ }
+
+ try {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setPackage(appId);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName(appId, className);
+
+ if (extras != null) {
+ Iterator keys = extras.keys();
+
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = extras.get(key);
+
+ if (value instanceof Integer) {
+ intent.putExtra(key, (Integer) value);
+ } else if (value instanceof Boolean) {
+ intent.putExtra(key, (Boolean) value);
+ } else if (value instanceof Double) {
+ intent.putExtra(key, (Double) value);
+ } else if (value instanceof Long) {
+ intent.putExtra(key, (Long) value);
+ } else if (value instanceof String) {
+ intent.putExtra(key, (String) value);
+ } else {
+ intent.putExtra(key, value.toString());
+ }
+ }
+ }
+
+ activity.startActivity(intent);
+ callback.success("Launched " + appId);
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void addShortcut(
+ String id,
+ String label,
+ String description,
+ String iconSrc,
+ String action,
+ String data,
+ CallbackContext callback
+ ) {
+ try {
+ Intent intent;
+ ImageDecoder.Source imgSrc;
+ Bitmap bitmap;
+ IconCompat icon;
+
+ imgSrc = ImageDecoder.createSource(
+ context.getContentResolver(),
+ Uri.parse(iconSrc)
+ );
+ bitmap = ImageDecoder.decodeBitmap(imgSrc);
+ icon = IconCompat.createWithBitmap(bitmap);
+ intent = activity
+ .getPackageManager()
+ .getLaunchIntentForPackage(activity.getPackageName());
+ intent.putExtra("action", action);
+ intent.putExtra("data", data);
+
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id)
+ .setShortLabel(label)
+ .setLongLabel(description)
+ .setIcon(icon)
+ .setIntent(intent)
+ .build();
+
+ ShortcutManagerCompat.pushDynamicShortcut(context, shortcut);
+ callback.success();
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void pinShortcut(String id, CallbackContext callback) {
+ ShortcutManager shortcutManager = context.getSystemService(
+ ShortcutManager.class
+ );
+
+ if (shortcutManager.isRequestPinShortcutSupported()) {
+ ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(
+ context,
+ id
+ ).build();
+
+ Intent pinnedShortcutCallbackIntent =
+ shortcutManager.createShortcutResultIntent(pinShortcutInfo);
+
+ PendingIntent successCallback = PendingIntent.getBroadcast(
+ context,
+ 0,
+ pinnedShortcutCallbackIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ );
+
+ shortcutManager.requestPinShortcut(
+ pinShortcutInfo,
+ successCallback.getIntentSender()
+ );
+
+ callback.success();
+ return;
+ }
+
+ callback.error("Not supported");
+ }
+
+ private void removeShortcut(String id, CallbackContext callback) {
+ try {
+ List list = new ArrayList();
+ list.add(id);
+ ShortcutManagerCompat.removeDynamicShortcuts(context, list);
+ callback.success();
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void setUiTheme(
+ final String systemBarColor,
+ final JSONObject scheme,
+ final CallbackContext callback
+ ) {
+ try {
+ this.systemBarColor = Color.parseColor(systemBarColor);
+ this.theme = new Theme(scheme);
+
+ preferences.set("BackgroundColor", this.systemBarColor);
+ webView.getPluginManager().postMessage("updateSystemBars", null);
+ applySystemBarTheme();
+
+ if (scheme != null) {
try {
- this.systemBarColor = Color.parseColor(systemBarColor);
- this.theme = new Theme(scheme);
-
- preferences.set("BackgroundColor", this.systemBarColor);
- webView.getPluginManager().postMessage("updateSystemBars", null);
- applySystemBarTheme();
-
- callback.success();
- } catch (IllegalArgumentException e) {
- callback.error("Invalid color: " + systemBarColor);
- } catch (Exception e) {
- callback.error(e.toString());
- }
- }
-
- private void applySystemBarTheme() {
- final Window window = activity.getWindow();
- final View decorView = window.getDecorView();
-
- // Keep Cordova's BackgroundColor flow for API 36+, but also apply the
- // window colors directly so OEM variants do not leave stale system-bar
- // colors behind after a theme switch.
- window.clearFlags(0x04000000 | 0x08000000); // FLAG_TRANSLUCENT_STATUS | FLAG_TRANSLUCENT_NAVIGATION
- window.addFlags(0x80000000); // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- window.setNavigationBarContrastEnforced(false);
- window.setStatusBarContrastEnforced(false);
- }
-
- decorView.setBackgroundColor(this.systemBarColor);
-
- View rootView = activity.findViewById(android.R.id.content);
- if (rootView != null) {
- rootView.setBackgroundColor(this.systemBarColor);
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- window.setStatusBarColor(this.systemBarColor);
- window.setNavigationBarColor(this.systemBarColor);
- }
-
- setStatusBarStyle(window);
- setNavigationBarStyle(window);
- }
-
- private void setStatusBarStyle(final Window window) {
- String themeType = theme.getType();
- View decorView = window.getDecorView();
- int uiOptions;
- int lightStatusBar;
-
- if (SDK_INT <= 30) {
- uiOptions = getDeprecatedSystemUiVisibility(decorView);
- lightStatusBar = deprecatedFlagUiLightStatusBar();
-
- if (themeType.equals("light")) {
- setDeprecatedSystemUiVisibility(decorView, uiOptions | lightStatusBar);
- return;
- }
- setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightStatusBar);
- return;
- }
-
- uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance();
- lightStatusBar = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-
- if (themeType.equals("light")) {
- decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightStatusBar, lightStatusBar);
- return;
- }
-
- decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions & ~lightStatusBar, lightStatusBar);
- }
-
- private void setNavigationBarStyle(final Window window) {
- String themeType = theme.getType();
- View decorView = window.getDecorView();
- int uiOptions;
- int lightNavigationBar;
-
- if (SDK_INT <= 30) {
- uiOptions = getDeprecatedSystemUiVisibility(decorView);
- lightNavigationBar = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
-
- if (themeType.equals("light")) {
- setDeprecatedSystemUiVisibility(decorView, uiOptions | lightNavigationBar);
- return;
- }
- setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightNavigationBar);
- return;
- }
-
- uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance();
- lightNavigationBar = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
-
- if (themeType.equals("light")) {
- decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightNavigationBar, lightNavigationBar);
- return;
- }
-
- decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions & ~lightNavigationBar, lightNavigationBar);
- }
-
- private int deprecatedFlagUiLightStatusBar() {
- return View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
- }
-
- private int getDeprecatedSystemUiVisibility(View decorView) {
- return decorView.getSystemUiVisibility();
- }
-
- private void setDeprecatedSystemUiVisibility(View decorView, int visibility) {
- decorView.setSystemUiVisibility(visibility);
- }
-
- private void getCordovaIntent(CallbackContext callback) {
- Intent intent = activity.getIntent();
- if (isReservedAuthIntent(intent)) {
- callback.sendPluginResult(
- new PluginResult(PluginResult.Status.OK, new JSONObject())
- );
- return;
- }
- callback.sendPluginResult(
- new PluginResult(PluginResult.Status.OK, getIntentJson(intent))
+ android.content.SharedPreferences themePrefs = activity
+ .getApplicationContext()
+ .getSharedPreferences("acode_theme", Context.MODE_PRIVATE);
+ android.content.SharedPreferences.Editor prefEditor = themePrefs.edit();
+ Iterator keys = scheme.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ prefEditor.putString(key, scheme.optString(key));
+ }
+ prefEditor.apply();
+ } catch (Exception ignored) {}
+ }
+
+ callback.success();
+ } catch (IllegalArgumentException e) {
+ callback.error("Invalid color: " + systemBarColor);
+ } catch (Exception e) {
+ callback.error(e.toString());
+ }
+ }
+
+ private void applySystemBarTheme() {
+ final Window window = activity.getWindow();
+ final View decorView = window.getDecorView();
+
+ // Keep Cordova's BackgroundColor flow for API 36+, but also apply the
+ // window colors directly so OEM variants do not leave stale system-bar
+ // colors behind after a theme switch.
+ window.clearFlags(0x04000000 | 0x08000000); // FLAG_TRANSLUCENT_STATUS | FLAG_TRANSLUCENT_NAVIGATION
+ window.addFlags(0x80000000); // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ window.setNavigationBarContrastEnforced(false);
+ window.setStatusBarContrastEnforced(false);
+ }
+
+ decorView.setBackgroundColor(this.systemBarColor);
+
+ View rootView = activity.findViewById(android.R.id.content);
+ if (rootView != null) {
+ rootView.setBackgroundColor(this.systemBarColor);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ window.setStatusBarColor(this.systemBarColor);
+ window.setNavigationBarColor(this.systemBarColor);
+ }
+
+ setStatusBarStyle(window);
+ setNavigationBarStyle(window);
+ }
+
+ private void setStatusBarStyle(final Window window) {
+ String themeType = theme.getType();
+ View decorView = window.getDecorView();
+ int uiOptions;
+ int lightStatusBar;
+
+ if (SDK_INT <= 30) {
+ uiOptions = getDeprecatedSystemUiVisibility(decorView);
+ lightStatusBar = deprecatedFlagUiLightStatusBar();
+
+ if (themeType.equals("light")) {
+ setDeprecatedSystemUiVisibility(decorView, uiOptions | lightStatusBar);
+ return;
+ }
+ setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightStatusBar);
+ return;
+ }
+
+ uiOptions = Objects.requireNonNull(
+ decorView.getWindowInsetsController()
+ ).getSystemBarsAppearance();
+ lightStatusBar = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+
+ if (themeType.equals("light")) {
+ decorView
+ .getWindowInsetsController()
+ .setSystemBarsAppearance(uiOptions | lightStatusBar, lightStatusBar);
+ return;
+ }
+
+ decorView
+ .getWindowInsetsController()
+ .setSystemBarsAppearance(uiOptions & ~lightStatusBar, lightStatusBar);
+ }
+
+ private void setNavigationBarStyle(final Window window) {
+ String themeType = theme.getType();
+ View decorView = window.getDecorView();
+ int uiOptions;
+ int lightNavigationBar;
+
+ if (SDK_INT <= 30) {
+ uiOptions = getDeprecatedSystemUiVisibility(decorView);
+ lightNavigationBar = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+
+ if (themeType.equals("light")) {
+ setDeprecatedSystemUiVisibility(
+ decorView,
+ uiOptions | lightNavigationBar
);
- }
-
- private void setIntentHandler(CallbackContext callback) {
- intentHandler = callback;
- PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
- result.setKeepCallback(true);
- callback.sendPluginResult(result);
- }
-
- @Override
- public void onNewIntent(Intent intent) {
- if (isReservedAuthIntent(intent)) {
- return;
- }
- if (intentHandler != null) {
- PluginResult result = new PluginResult(
- PluginResult.Status.OK,
- getIntentJson(intent)
- );
- result.setKeepCallback(true);
- intentHandler.sendPluginResult(result);
- }
- }
-
- private boolean isReservedAuthIntent(Intent intent) {
- Uri data = intent != null ? intent.getData() : null;
- if (data == null || !"acode".equals(data.getScheme())) {
- return false;
- }
- String host = data.getHost();
- String path = data.getPath();
- return "auth".equals(host) && "/callback".equals(path);
- }
-
- private JSONObject getIntentJson(Intent intent) {
- JSONObject json = new JSONObject();
+ return;
+ }
+ setDeprecatedSystemUiVisibility(
+ decorView,
+ uiOptions & ~lightNavigationBar
+ );
+ return;
+ }
+
+ uiOptions = Objects.requireNonNull(
+ decorView.getWindowInsetsController()
+ ).getSystemBarsAppearance();
+ lightNavigationBar =
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
+
+ if (themeType.equals("light")) {
+ decorView
+ .getWindowInsetsController()
+ .setSystemBarsAppearance(
+ uiOptions | lightNavigationBar,
+ lightNavigationBar
+ );
+ return;
+ }
+
+ decorView
+ .getWindowInsetsController()
+ .setSystemBarsAppearance(
+ uiOptions & ~lightNavigationBar,
+ lightNavigationBar
+ );
+ }
+
+ private int deprecatedFlagUiLightStatusBar() {
+ return View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ }
+
+ private int getDeprecatedSystemUiVisibility(View decorView) {
+ return decorView.getSystemUiVisibility();
+ }
+
+ private void setDeprecatedSystemUiVisibility(View decorView, int visibility) {
+ decorView.setSystemUiVisibility(visibility);
+ }
+
+ private void getCordovaIntent(CallbackContext callback) {
+ Intent intent = activity.getIntent();
+ if (isReservedAuthIntent(intent)) {
+ callback.sendPluginResult(
+ new PluginResult(PluginResult.Status.OK, new JSONObject())
+ );
+ return;
+ }
+ callback.sendPluginResult(
+ new PluginResult(PluginResult.Status.OK, getIntentJson(intent))
+ );
+ }
+
+ private void setIntentHandler(CallbackContext callback) {
+ intentHandler = callback;
+ PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
+ result.setKeepCallback(true);
+ callback.sendPluginResult(result);
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ if (isReservedAuthIntent(intent)) {
+ return;
+ }
+ if (intentHandler != null) {
+ PluginResult result = new PluginResult(
+ PluginResult.Status.OK,
+ getIntentJson(intent)
+ );
+ result.setKeepCallback(true);
+ intentHandler.sendPluginResult(result);
+ }
+ }
+
+ private boolean isReservedAuthIntent(Intent intent) {
+ Uri data = intent != null ? intent.getData() : null;
+ if (data == null || !"acode".equals(data.getScheme())) {
+ return false;
+ }
+ String host = data.getHost();
+ String path = data.getPath();
+ return "auth".equals(host) && "/callback".equals(path);
+ }
+
+ private JSONObject getIntentJson(Intent intent) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put("action", intent.getAction());
+ json.put("data", intent.getDataString());
+ json.put("type", intent.getType());
+ json.put("package", intent.getPackage());
+ json.put("extras", getExtrasJson(intent.getExtras()));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return json;
+ }
+
+ private JSONObject getExtrasJson(Bundle extras) {
+ JSONObject json = new JSONObject();
+ if (extras != null) {
+ for (String key : extras.keySet()) {
try {
- json.put("action", intent.getAction());
- json.put("data", intent.getDataString());
- json.put("type", intent.getType());
- json.put("package", intent.getPackage());
- json.put("extras", getExtrasJson(intent.getExtras()));
+ Object value = extras.get(key);
+ if (value instanceof String) {
+ json.put(key, (String) value);
+ } else if (value instanceof Integer) {
+ json.put(key, (Integer) value);
+ } else if (value instanceof Long) {
+ json.put(key, (Long) value);
+ } else if (value instanceof Double) {
+ json.put(key, (Double) value);
+ } else if (value instanceof Float) {
+ json.put(key, (Float) value);
+ } else if (value instanceof Boolean) {
+ json.put(key, (Boolean) value);
+ } else if (value instanceof Bundle) {
+ json.put(key, getExtrasJson((Bundle) value));
+ } else {
+ json.put(key, value.toString());
+ }
} catch (JSONException e) {
- e.printStackTrace();
+ e.printStackTrace();
}
- return json;
+ }
}
+ return json;
+ }
- private JSONObject getExtrasJson(Bundle extras) {
- JSONObject json = new JSONObject();
- if (extras != null) {
- for (String key: extras.keySet()) {
- try {
- Object value = extras.get(key);
- if (value instanceof String) {
- json.put(key, (String) value);
- } else if (value instanceof Integer) {
- json.put(key, (Integer) value);
- } else if (value instanceof Long) {
- json.put(key, (Long) value);
- } else if (value instanceof Double) {
- json.put(key, (Double) value);
- } else if (value instanceof Float) {
- json.put(key, (Float) value);
- } else if (value instanceof Boolean) {
- json.put(key, (Boolean) value);
- } else if (value instanceof Bundle) {
- json.put(key, getExtrasJson((Bundle) value));
- } else {
- json.put(key, value.toString());
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- }
- return json;
- }
-
- private Uri getContentProviderUri(String fileUri) {
- return this.getContentProviderUri(fileUri, "");
- }
-
- private Uri getContentProviderUri(String fileUri, String filename) {
- if (fileUri == null || fileUri.isEmpty()) {
- return null;
- }
-
- Uri uri = Uri.parse(fileUri);
- if (uri == null) {
- return null;
- }
-
- if ("file".equalsIgnoreCase(uri.getScheme())) {
- File originalFile = new File(uri.getPath());
- if (!originalFile.exists()) {
- Log.e("System", "File does not exist for URI: " + fileUri);
- return null;
- }
+ private Uri getContentProviderUri(String fileUri) {
+ return this.getContentProviderUri(fileUri, "");
+ }
- String authority = getFileProviderAuthority();
- if (authority == null) {
- Log.e("System", "No FileProvider authority available.");
- return null;
- }
-
- try {
- return FileProvider.getUriForFile(context, authority, originalFile);
- } catch (IllegalArgumentException | SecurityException ex) {
- try {
- File cacheCopy = ensureShareableCopy(originalFile, filename);
- return FileProvider.getUriForFile(context, authority, cacheCopy);
- } catch (Exception copyError) {
- Log.e("System", "Failed to expose file via FileProvider", copyError);
- return null;
- }
- }
- }
- return uri;
+ private Uri getContentProviderUri(String fileUri, String filename) {
+ if (fileUri == null || fileUri.isEmpty()) {
+ return null;
}
- private File ensureShareableCopy(File source, String displayName) throws IOException {
- File cacheRoot = new File(context.getCacheDir(), "shared");
- if (!cacheRoot.exists() && !cacheRoot.mkdirs()) {
- throw new IOException("Unable to create shared cache directory");
- }
-
- if (displayName != null && !displayName.isEmpty()) {
- displayName = new File(displayName).getName();
- }
- if (displayName == null || displayName.isEmpty()) {
- displayName = source.getName();
- }
- if (displayName == null || displayName.isEmpty()) {
- displayName = "shared-file";
- }
-
- File target = new File(cacheRoot, displayName);
- target = ensureUniqueFile(target);
- copyFile(source, target);
- return target;
+ Uri uri = Uri.parse(fileUri);
+ if (uri == null) {
+ return null;
}
- private File ensureUniqueFile(File target) {
- if (!target.exists()) {
- return target;
- }
+ if ("file".equalsIgnoreCase(uri.getScheme())) {
+ File originalFile = new File(uri.getPath());
+ if (!originalFile.exists()) {
+ Log.e("System", "File does not exist for URI: " + fileUri);
+ return null;
+ }
- String name = target.getName();
- String prefix = name;
- String suffix = "";
- int dotIndex = name.lastIndexOf('.');
- if (dotIndex > 0) {
- prefix = name.substring(0, dotIndex);
- suffix = name.substring(dotIndex);
- }
+ String authority = getFileProviderAuthority();
+ if (authority == null) {
+ Log.e("System", "No FileProvider authority available.");
+ return null;
+ }
- int index = 1;
- File candidate = target;
- while (candidate.exists()) {
- candidate = new File(target.getParentFile(), prefix + "-" + index + suffix);
- index++;
- }
- return candidate;
- }
-
- private void copyFile(File source, File destination) throws IOException {
- try (
- InputStream in = new FileInputStream(source);
- OutputStream out = new FileOutputStream(destination)
- ) {
- byte[] buffer = new byte[8192];
- int length;
- while ((length = in.read(buffer)) != -1) {
- out.write(buffer, 0, length);
- }
- out.flush();
+ try {
+ return FileProvider.getUriForFile(context, authority, originalFile);
+ } catch (IllegalArgumentException | SecurityException ex) {
+ try {
+ File cacheCopy = ensureShareableCopy(originalFile, filename);
+ return FileProvider.getUriForFile(context, authority, cacheCopy);
+ } catch (Exception copyError) {
+ Log.e("System", "Failed to expose file via FileProvider", copyError);
+ return null;
}
+ }
}
+ return uri;
+ }
- private void grantUriPermissions(Intent intent, Uri uri, int flags) {
- if (uri == null) return;
- PackageManager pm = context.getPackageManager();
- List resInfoList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
- for (ResolveInfo resolveInfo: resInfoList) {
- String packageName = resolveInfo.activityInfo.packageName;
- context.grantUriPermission(packageName, uri, flags);
- }
+ private File ensureShareableCopy(File source, String displayName)
+ throws IOException {
+ File cacheRoot = new File(context.getCacheDir(), "shared");
+ if (!cacheRoot.exists() && !cacheRoot.mkdirs()) {
+ throw new IOException("Unable to create shared cache directory");
}
- private String resolveMimeType(String currentMime, Uri uri, String filename) {
- if (currentMime != null && !currentMime.isEmpty() && !currentMime.equals("*/*")) {
- return currentMime;
- }
-
- String mime = null;
- if (uri != null) {
- mime = context.getContentResolver().getType(uri);
- }
-
- if ((mime == null || mime.isEmpty()) && filename != null) {
- mime = getMimeTypeFromExtension(filename);
- }
-
- if ((mime == null || mime.isEmpty()) && uri != null) {
- String path = uri.getPath();
- if (path != null) {
- mime = getMimeTypeFromExtension(path);
- }
- }
-
- return (mime != null && !mime.isEmpty()) ? mime : "*/*";
+ if (displayName != null && !displayName.isEmpty()) {
+ displayName = new File(displayName).getName();
}
-
- private String getFileProviderAuthority() {
- if (fileProviderAuthority != null && !fileProviderAuthority.isEmpty()) {
- return fileProviderAuthority;
- }
-
- try {
- PackageManager pm = context.getPackageManager();
- PackageInfo packageInfo = pm.getPackageInfo(
- context.getPackageName(),
- PackageManager.GET_PROVIDERS
- );
- if (packageInfo.providers != null) {
- for (ProviderInfo providerInfo: packageInfo.providers) {
- if (
- providerInfo != null &&
- providerInfo.name != null &&
- providerInfo.name.equals(FileProvider.class.getName())
- ) {
- fileProviderAuthority = providerInfo.authority;
- break;
- }
- }
- }
- } catch (PackageManager.NameNotFoundException error) {
- Log.w(TAG, "Unable to inspect package providers for FileProvider authority.", error);
- }
-
- if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) {
- fileProviderAuthority = context.getPackageName() + ".provider";
- }
-
- return fileProviderAuthority;
+ if (displayName == null || displayName.isEmpty()) {
+ displayName = source.getName();
}
-
- private boolean isPackageInstalled(
- String packageName,
- PackageManager packageManager,
- CallbackContext callback
- ) {
- try {
- packageManager.getPackageInfo(packageName, 0);
- return true;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
+ if (displayName == null || displayName.isEmpty()) {
+ displayName = "shared-file";
}
- private void getGlobalSetting(String setting, CallbackContext callback) {
- int value = (int) Global.getFloat(
- context.getContentResolver(),
- setting, -1
- );
- callback.success(value);
- }
+ File target = new File(cacheRoot, displayName);
+ target = ensureUniqueFile(target);
+ copyFile(source, target);
+ return target;
+ }
- private void clearCache(CallbackContext callback) {
- webView.clearCache(true);
- callback.success("Cache cleared");
+ private File ensureUniqueFile(File target) {
+ if (!target.exists()) {
+ return target;
}
- private void setInputType(String type) {
- int mode = -1;
- if (type.equals("NO_SUGGESTIONS")) {
- mode = 0;
- } else if (type.equals("NO_SUGGESTIONS_AGGRESSIVE")) {
- mode = 1;
- }
- webView.setInputType(mode);
+ String name = target.getName();
+ String prefix = name;
+ String suffix = "";
+ int dotIndex = name.lastIndexOf('.');
+ if (dotIndex > 0) {
+ prefix = name.substring(0, dotIndex);
+ suffix = name.substring(dotIndex);
}
- private void setNativeContextMenuDisabled(boolean disabled) {
- if (webView == null) {
- return;
- }
- webView.setNativeContextMenuDisabled(disabled);
+ int index = 1;
+ File candidate = target;
+ while (candidate.exists()) {
+ candidate = new File(
+ target.getParentFile(),
+ prefix + "-" + index + suffix
+ );
+ index++;
}
+ return candidate;
+ }
- private void extractAsset(String assetName, String destinationPath, CallbackContext callback) {
- try (
- InputStream in = context.getAssets().open(assetName);
- OutputStream out = new FileOutputStream(destinationPath)
- ) {
- byte[] buffer = new byte[8192];
- int length;
- while ((length = in.read(buffer)) != -1) {
- out.write(buffer, 0, length);
- }
- out.flush();
- callback.success();
- }catch (IOException e) {
- StringWriter sw = new StringWriter();
- e.printStackTrace(new PrintWriter(sw));
- callback.error(sw.toString());
- }
- }
+ private void copyFile(File source, File destination) throws IOException {
+ try (
+ InputStream in = new FileInputStream(source);
+ OutputStream out = new FileOutputStream(destination)
+ ) {
+ byte[] buffer = new byte[8192];
+ int length;
+ while ((length = in.read(buffer)) != -1) {
+ out.write(buffer, 0, length);
+ }
+ out.flush();
+ }
+ }
+
+ private void grantUriPermissions(Intent intent, Uri uri, int flags) {
+ if (uri == null) return;
+ PackageManager pm = context.getPackageManager();
+ List resInfoList = pm.queryIntentActivities(
+ intent,
+ PackageManager.MATCH_DEFAULT_ONLY
+ );
+ for (ResolveInfo resolveInfo : resInfoList) {
+ String packageName = resolveInfo.activityInfo.packageName;
+ context.grantUriPermission(packageName, uri, flags);
+ }
+ }
+
+ private String resolveMimeType(String currentMime, Uri uri, String filename) {
+ if (
+ currentMime != null &&
+ !currentMime.isEmpty() &&
+ !currentMime.equals("*/*")
+ ) {
+ return currentMime;
+ }
+
+ String mime = null;
+ if (uri != null) {
+ mime = context.getContentResolver().getType(uri);
+ }
+
+ if ((mime == null || mime.isEmpty()) && filename != null) {
+ mime = getMimeTypeFromExtension(filename);
+ }
+
+ if ((mime == null || mime.isEmpty()) && uri != null) {
+ String path = uri.getPath();
+ if (path != null) {
+ mime = getMimeTypeFromExtension(path);
+ }
+ }
+
+ return (mime != null && !mime.isEmpty()) ? mime : "*/*";
+ }
+
+ private String getFileProviderAuthority() {
+ if (fileProviderAuthority != null && !fileProviderAuthority.isEmpty()) {
+ return fileProviderAuthority;
+ }
+
+ try {
+ PackageManager pm = context.getPackageManager();
+ PackageInfo packageInfo = pm.getPackageInfo(
+ context.getPackageName(),
+ PackageManager.GET_PROVIDERS
+ );
+ if (packageInfo.providers != null) {
+ for (ProviderInfo providerInfo : packageInfo.providers) {
+ if (
+ providerInfo != null &&
+ providerInfo.name != null &&
+ providerInfo.name.equals(FileProvider.class.getName())
+ ) {
+ fileProviderAuthority = providerInfo.authority;
+ break;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException error) {
+ Log.w(
+ TAG,
+ "Unable to inspect package providers for FileProvider authority.",
+ error
+ );
+ }
+
+ if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) {
+ fileProviderAuthority = context.getPackageName() + ".provider";
+ }
+
+ return fileProviderAuthority;
+ }
+
+ private boolean isPackageInstalled(
+ String packageName,
+ PackageManager packageManager,
+ CallbackContext callback
+ ) {
+ try {
+ packageManager.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void getGlobalSetting(String setting, CallbackContext callback) {
+ int value = (int) Global.getFloat(
+ context.getContentResolver(),
+ setting,
+ -1
+ );
+ callback.success(value);
+ }
+
+ private void clearCache(CallbackContext callback) {
+ webView.clearCache(true);
+ callback.success("Cache cleared");
+ }
+
+ private void setInputType(String type) {
+ int mode = -1;
+ if (type.equals("NO_SUGGESTIONS")) {
+ mode = 0;
+ } else if (type.equals("NO_SUGGESTIONS_AGGRESSIVE")) {
+ mode = 1;
+ }
+ webView.setInputType(mode);
+ }
+
+ private void setNativeContextMenuDisabled(boolean disabled) {
+ if (webView == null) {
+ return;
+ }
+ webView.setNativeContextMenuDisabled(disabled);
+ }
+
+ private void extractAsset(
+ String assetName,
+ String destinationPath,
+ CallbackContext callback
+ ) {
+ try (
+ InputStream in = context.getAssets().open(assetName);
+ OutputStream out = new FileOutputStream(destinationPath)
+ ) {
+ byte[] buffer = new byte[8192];
+ int length;
+ while ((length = in.read(buffer)) != -1) {
+ out.write(buffer, 0, length);
+ }
+ out.flush();
+ callback.success();
+ } catch (IOException e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ callback.error(sw.toString());
+ }
+ }
}