From 9599de8c35f2b1d7dd9b18d1f7923f096ef50718 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Sun, 5 Jul 2026 14:10:10 +0530 Subject: [PATCH 1/7] feat: added native crash handler --- package-lock.json | 10 + package.json | 6 +- .../cordova-plugin-crashhandler/package.json | 19 + .../cordova-plugin-crashhandler/plugin.xml | 32 + .../src/android/CrashActivity.java | 338 ++ .../src/android/CrashHandler.java | 56 + .../android/com/foxdebug/system/System.java | 4082 +++++++++-------- 7 files changed, 2512 insertions(+), 2031 deletions(-) create mode 100644 src/plugins/cordova-plugin-crashhandler/package.json create mode 100644 src/plugins/cordova-plugin-crashhandler/plugin.xml create mode 100644 src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java create mode 100644 src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java 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..240d89f83 --- /dev/null +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java @@ -0,0 +1,338 @@ +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.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.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class CrashActivity extends Activity { + + private String errorType; + private String errorMessage; + private String stackTrace; + private String fullReport; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Retrieve data from intent + 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."; + + // Build system information + 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 timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + + // Construct full report for copying + fullReport = "Acode Crash Report\n" + + "==================\n" + + "Time: " + timestamp + "\n" + + "Error Type: " + errorType + "\n" + + "Error Message: " + errorMessage + "\n" + + "App Version: " + appVersion + " (" + appBuild + ")\n" + + "Device: " + deviceName + "\n" + + "Android Version: " + androidVersion + "\n\n" + + "Stack Trace:\n" + + stackTrace; + + // --- Build UI Programmatically (Acode Theme Integration) --- + // Main Container ScrollView + ScrollView mainScrollView = new ScrollView(this); + mainScrollView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mainScrollView.setBackgroundColor(Color.parseColor("#23272a")); // Acode Primary Dark BG + mainScrollView.setFillViewport(true); + + // Vertical Content Container + 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); + + // Header Warning Title + TextView titleView = new TextView(this); + titleView.setText("Acode Crashed"); + titleView.setTextSize(24); + titleView.setTextColor(Color.parseColor("#f5f5f5")); // Acode Primary Text + 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); + + // Explanation text + 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(Color.parseColor("#e4e4e4")); // Acode Secondary Text + 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); + + // --- System Metadata Section --- + TextView metaTitleView = new TextView(this); + metaTitleView.setText("DEVICE & APP INFO"); + metaTitleView.setTextSize(11); + metaTitleView.setTextColor(Color.parseColor("#8ab4f8")); // Acode Link Text color (light blue) + 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(Color.parseColor("#2d3134")); // Acode Secondary Panel BG + cardBg.setCornerRadius(dp(4)); // Acode standard border radius + cardBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border color + 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("Time", timestamp)); + metaCard.addView(createMetaRow("Error Type", errorType)); + rootLayout.addView(metaCard); + + // --- Stack Trace Section --- + TextView logsTitleView = new TextView(this); + logsTitleView.setText("STACK TRACE"); + logsTitleView.setTextSize(11); + logsTitleView.setTextColor(Color.parseColor("#8ab4f8")); + 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); + + // Trace card + 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(Color.parseColor("#181a1f")); // Monospace editor background + traceBg.setCornerRadius(dp(4)); + traceBg.setStroke(dp(1), Color.parseColor("#3a3e46")); + traceCard.setBackground(traceBg); + + LinearLayout.LayoutParams traceCardParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + dp(280)); // Fixed height box for logs + 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(Color.parseColor("#e4e4e4")); // Clean light-grey code text + traceView.setTypeface(Typeface.MONOSPACE); + traceView.setHorizontallyScrolling(true); + traceView.setTextIsSelectable(true); + + traceHorizontalScroll.addView(traceView); + traceVerticalScroll.addView(traceHorizontalScroll); + traceCard.addView(traceVerticalScroll); + rootLayout.addView(traceCard); + + // --- Buttons Section (Clean Acode style) --- + 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); + + // Restart Button (Primary Action - Acode Blue theme color) + TextView btnRestart = createButton("Restart Acode", "#ffffff", "#4285f4", 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); + + // Copy Button (Secondary Action - Flat dark panel with border) + TextView btnCopy = createButton("Copy Error Details", "#e4e4e4", "#2d3134", 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); + + // Close Button (Tertiary Action - Flat dark panel with border) + TextView btnClose = createButton("Close", "#e4e4e4", "#2d3134", 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 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(Color.parseColor("#a6accd")); + 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(Color.parseColor("#eeffff")); + 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, String textColorHex, String bgHex, boolean hasBorder) { + final TextView btn = new TextView(this); + btn.setText(text); + btn.setTextSize(15); + btn.setTextColor(Color.parseColor(textColorHex)); + 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(Color.parseColor(bgHex)); + normalBg.setCornerRadius(dp(4)); // Standard Acode popup radius + + if (hasBorder) { + normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style + } + + 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); + + // Tactile touch animations + 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 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..d9afa2818 --- /dev/null +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java @@ -0,0 +1,56 @@ +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 (Exception 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..4362b07f0 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -2,39 +2,80 @@ 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.*; import android.content.ClipData; import android.content.Context; +import android.content.Context; +import android.content.Context; +import android.content.Intent; +import android.content.Intent; import android.content.Intent; -import android.content.res.Configuration; -import android.content.*; import android.content.pm.*; -import java.util.*; +import android.content.pm.InstallSourceInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ImageDecoder; +import android.graphics.ImageDecoder; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.net.Uri; import android.net.Uri; +import android.net.Uri; +import android.os.Build; +import android.os.Build; +import android.os.Build; 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; +// DocumentFile import (AndroidX library) +import androidx.documentfile.provider.DocumentFile; import com.foxdebug.system.Ui.Theme; +import java.io.BufferedReader; +import java.io.File; +import java.io.File; +// Java I/O imports import java.io.File; +import java.io.FileInputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.IOException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; @@ -42,2165 +83,2150 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.file.Files; +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.security.MessageDigest; +import java.util.*; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.cordova.CallbackContext; +import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; 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; - -// DocumentFile import (AndroidX library) -import androidx.documentfile.provider.DocumentFile; - -// 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; - -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; - -import androidx.core.content.ContextCompat; - - -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.graphics.Canvas; -import android.util.TypedValue; - -import android.provider.MediaStore; -import android.graphics.Bitmap; -import android.os.Build; -import android.graphics.ImageDecoder; - - -import java.security.MessageDigest; -import java.security.MessageDigest; - -import android.content.pm.InstallSourceInfo; -import android.content.pm.PackageManager; -import android.os.Build; - -import android.content.Intent; - -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; - import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONException; +import org.json.JSONObject; 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); - } - } - ); - - // 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 - ); - - sendLogToJavaScript("error", errorMsg); - // rethrow to the default handler - Thread.getDefaultUncaughtExceptionHandler() - .uncaughtException(thread, ex); - } + 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(); } + } ); - } - - 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); - - 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; - } - - 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; - } - - 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()); - } - - 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(); - - 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; - } - } - } - ); - 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) + ");"; + 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 { - ((android.webkit.WebView) webView.getEngine().getView()) - .evaluateJavascript(js, null); - } catch (Exception e) { - Log.e(TAG, "Failed to send log to JavaScript: " + e.getMessage()); - } + 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()); + } }); - } - - // 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()); - } - } - - private void decode( - String content, // base64 encoded string - String charSetName, - CallbackContext callback - ) { - try { - byte[] bytes = Base64.decode(content, Base64.DEFAULT); - - 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; - } - - 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()); - } - } - - /** - * 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 + return true; + case "set-ui-theme": + this.cordova.getActivity().runOnUiThread( + new Runnable() { + public void run() { + setUiTheme(arg1, args.optJSONObject(1), callbackContext); } - - } 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); + } + ); + return true; + case "clear-cache": + this.cordova.getActivity().runOnUiThread( + new Runnable() { + public void run() { + clearCache(callbackContext); } - callback.success(result); - } catch (Exception e) { - callback.error(e.toString()); - } - } - - private void requestPermissions(JSONArray arr, CallbackContext callback) { + } + ); + 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 { - 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; - } - - 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()); - } - } - - 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; - } - - callback.success(1); - return; - } - - callback.error("No permission passed to request."); - } + String filePath = args.getString(0); + String content = args.getString(1); - 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; - } - } + Files.write( + Paths.get(filePath), + Collections.singleton(content), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ); - public boolean createSymlink(String target, String linkPath) { - try { - Process process = Runtime.getRuntime().exec(new String[] { - "ln", - "-s", - target, - linkPath - }); - return process.waitFor() == 0; + callbackContext.success("File written successfully"); } 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; + callbackContext.error("Failed to write file: " + e.getMessage()); } + return true; + } + case "getArch": + String arch; - 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 < 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); - } + if (android.os.Build.VERSION.SDK_INT >= 21) { + arch = android.os.Build.SUPPORTED_ABIS[0]; + } else { + arch = android.os.Build.CPU_ABI; } - 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 { - 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() + ")" + 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 ); - - 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 + 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 ); - - 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()); - } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + callbackContext.success("true"); + } + } else { + callbackContext.success("false"); // Not needed on Android < 11 } - - 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 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; + } } - return filename.substring(0, 1).toUpperCase(Locale.getDefault()); + } + callbackContext.success(isDeclared); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + callbackContext.error(e.toString()); } - 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]; + return true; + case "mkdirs": + if (new File(args.getString(0)).mkdirs()) { + callbackContext.success(); + } else { + callbackContext.error("mkdirs failed"); } - } - - 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; + 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"); } - try { - Intent intent = new Intent(action); - - if (mimeType.equals("")) { - mimeType = "text/plain"; - } - mimeType = resolveMimeType(mimeType, uri, filename); + return true; + default: + return false; + } - 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 - ) + 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() ); - 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; + } + return; + case "getInstaller": + try { + PackageManager pm = context.getPackageManager(); - if (action.equals(Intent.ACTION_EDIT)) { - flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - } + String installer; - 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); - } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + InstallSourceInfo info = pm.getInstallSourceInfo( + context.getPackageName() + ); - 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); + installer = info.getInstallingPackageName(); + } else { + installer = pm.getInstallerPackageName( + context.getPackageName() + ); } - 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()); - } - } + 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)); - private void openInBrowser(String src, CallbackContext callback) { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(src)); - activity.startActivity(browserIntent); - } + //destUri is a directory + Uri destUri = Uri.parse(args.getString(1)); - private void launchApp( - String appId, - String className, - JSONObject extras, - CallbackContext callback - ) { - if (appId == null || appId.equals("")) { - callback.error("No package name provided."); - return; - } + //create a file named this into the dest Directory and copy the srcUri into it + String fileName = args.getString(2); - if (className == null || className.equals("")) { - callback.error("No activity class name provided."); - return; - } + 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); - 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()); + // Check if file already exists and delete it + DocumentFile existingFile = destDocDir.findFile(fileName); + if (existingFile != null && existingFile.exists()) { + existingFile.delete(); } - } - } - activity.startActivity(intent); - callback.success("Launched " + appId); + // Create new file + String mimeType = getMimeTypeFromExtension(fileName); + destFile = destDocDir.createFile(mimeType, fileName); + } else { + // Handle content:// scheme using DocumentFile + DocumentFile destDocDir = DocumentFile.fromTreeUri( + context, + destUri + ); - } catch (Exception e) { - callback.error(e.toString()); - } + 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); + } - 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()); - } - } + if (destFile == null || !destFile.exists()) { + callbackContext.error("Failed to create destination file"); + return; + } - private void pinShortcut(String id, CallbackContext callback) { - ShortcutManager shortcutManager = context.getSystemService( - ShortcutManager.class - ); + // Open output stream to the created file + out = context + .getContentResolver() + .openOutputStream(destFile.getUri()); - if (shortcutManager.isRequestPinShortcutSupported()) { - ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder( - context, - id - ).build(); + 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"); - Intent pinnedShortcutCallbackIntent = - shortcutManager.createShortcutResultIntent(pinShortcutInfo); + byte[] hash = digest.digest( + args.getString(0).getBytes("UTF-8") + ); - PendingIntent successCallback = PendingIntent.getBroadcast( - context, - 0, - pinnedShortcutCallbackIntent, - PendingIntent.FLAG_IMMUTABLE - ); + StringBuilder hexString = new StringBuilder(); - shortcutManager.requestPinShortcut( - pinShortcutInfo, - successCallback.getIntentSender() - ); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); - callback.success(); - return; - } + if (hex.length() == 1) hexString.append('0'); - callback.error("Not supported"); - } + hexString.append(hex); + } - 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()); - } - } + callbackContext.success(hexString.toString()); + } catch (Exception e) { + callbackContext.error(e.getMessage()); + } + }); - private void setUiTheme( - final String systemBarColor, - final JSONObject scheme, - final CallbackContext callback + 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 ) { - try { - this.systemBarColor = Color.parseColor(systemBarColor); - this.theme = new Theme(scheme); + callback.error(error.toString()); + } + } - preferences.set("BackgroundColor", this.systemBarColor); - webView.getPluginManager().postMessage("updateSystemBars", null); - applySystemBarTheme(); + private void decode( + String content, // base64 encoded string + String charSetName, + CallbackContext callback + ) { + try { + byte[] bytes = Base64.decode(content, Base64.DEFAULT); - callback.success(); - } catch (IllegalArgumentException e) { - callback.error("Invalid color: " + systemBarColor); - } catch (Exception e) { - callback.error(e.toString()); - } - } + 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; + } - private void applySystemBarTheme() { - final Window window = activity.getWindow(); - final View decorView = window.getDecorView(); + if (encoding == null || encoding.isEmpty()) { + encoding = "UTF-8"; + } - // 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 (!Charset.isSupported(encoding)) { + callback.error("Charset not supported: " + encoding); + return; + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.setNavigationBarContrastEnforced(false); - window.setStatusBarContrastEnforced(false); - } + Uri uri = Uri.parse(fileUri); + Charset charset = Charset.forName(encoding); + String fileContent; - decorView.setBackgroundColor(this.systemBarColor); + // Handle file:// URIs + if ("file".equalsIgnoreCase(uri.getScheme())) { + File file = new File(uri.getPath()); - View rootView = activity.findViewById(android.R.id.content); - if (rootView != null) { - rootView.setBackgroundColor(this.systemBarColor); + // Validate file + if (!file.exists()) { + callback.error("File does not exist"); + return; } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - window.setStatusBarColor(this.systemBarColor); - window.setNavigationBarColor(this.systemBarColor); + if (!file.isFile()) { + callback.error("Path is not a file"); + return; + } + if (!file.canRead()) { + callback.error("File is not readable"); + return; } - 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(); + 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; - if (themeType.equals("light")) { - setDeprecatedSystemUiVisibility(decorView, uiOptions | lightStatusBar); - return; + // 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; } - setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightStatusBar); - return; - } + } - uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance(); - lightStatusBar = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; + // Try to open the resolved URI + inputStream = context + .getContentResolver() + .openInputStream(resolvedUri); - if (themeType.equals("light")) { - decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightStatusBar, lightStatusBar); + if (inputStream == null) { + callback.error("Cannot open file"); 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; + } + + 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); } - setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightNavigationBar); - return; + } + 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; + } - uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance(); - lightNavigationBar = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; - - if (themeType.equals("light")) { - decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightNavigationBar, lightNavigationBar); - 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()); + } + } + + /** + * 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; + } - decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions & ~lightNavigationBar, lightNavigationBar); - } + String[] permissions = checkPermissions(arr); - private int deprecatedFlagUiLightStatusBar() { - return View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + 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 int getDeprecatedSystemUiVisibility(View decorView) { - return decorView.getSystemUiVisibility(); + private void requestPermission(String permission, CallbackContext callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + callback.success(1); + return; } - private void setDeprecatedSystemUiVisibility(View decorView, int visibility) { - decorView.setSystemUiVisibility(visibility); - } + 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(); - private void getCordovaIntent(CallbackContext callback) { - Intent intent = activity.getIntent(); - if (isReservedAuthIntent(intent)) { - callback.sendPluginResult( - new PluginResult(PluginResult.Status.OK, new JSONObject()) - ); - return; + try { + info = packageManager.getPackageInfo("com.google.android.webview", 0); + } catch (PackageManager.NameNotFoundException e) { + callback.error("Package not found"); } - 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); - } + 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() + ")" + ); - @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); - } + return; } + } - 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 void isPowerSaveMode(CallbackContext callback) { + PowerManager powerManager = (PowerManager) context.getSystemService( + Context.POWER_SERVICE + ); + boolean powerSaveMode = powerManager.isPowerSaveMode(); - 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; - } + callback.success(powerSaveMode ? 1 : 0); + } - 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 void pinFileShortcut( + JSONObject shortcutJson, + CallbackContext callback + ) { + if (shortcutJson == null) { + callback.error("Invalid shortcut data"); + return; } - private Uri getContentProviderUri(String fileUri) { - return this.getContentProviderUri(fileUri, ""); - } + 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", ""); - private Uri getContentProviderUri(String fileUri, String filename) { - if (fileUri == null || fileUri.isEmpty()) { - return null; - } + if (id.isEmpty() || label.isEmpty() || uriString.isEmpty()) { + callback.error("Missing required shortcut fields"); + return; + } - Uri uri = Uri.parse(fileUri); - if (uri == null) { - return null; - } + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + callback.error("Pin shortcut not supported on this launcher"); + return; + } - 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; - } + try { + Uri dataUri = Uri.parse(uriString); + String packageName = context.getPackageName(); + PackageManager pm = context.getPackageManager(); - String authority = getFileProviderAuthority(); - if (authority == null) { - Log.e("System", "No FileProvider authority available."); - return null; - } + Intent launchIntent = pm.getLaunchIntentForPackage(packageName); + if (launchIntent == null) { + callback.error("Launch intent not found for package: " + packageName); + return; + } - 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; - } + ComponentName componentName = launchIntent.getComponent(); + if (componentName == null) { + callback.error("ComponentName is null"); + return; + } - 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"); - } + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setComponent(componentName); + intent.setData(dataUri); + intent.putExtra("acodeFileUri", uriString); - 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"; - } + IconCompat icon; - File target = new File(cacheRoot, displayName); - target = ensureUniqueFile(target); - copyFile(source, target); - return target; - } + 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 + ); + } - private File ensureUniqueFile(File target) { - if (!target.exists()) { - return target; + 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 + ); + + 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) + ); + 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; - 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); + if (action.equals(Intent.ACTION_EDIT)) { + flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } - int index = 1; - File candidate = target; - while (candidate.exists()) { - candidate = new File(target.getParentFile(), prefix + "-" + index + suffix); - index++; - } - return candidate; + 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 + ); + + 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(); + + 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); } - - 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(); - } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.setStatusBarColor(this.systemBarColor); + window.setNavigationBarColor(this.systemBarColor); } - 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); - } - } + setStatusBarStyle(window); + setNavigationBarStyle(window); + } - private String resolveMimeType(String currentMime, Uri uri, String filename) { - if (currentMime != null && !currentMime.isEmpty() && !currentMime.equals("*/*")) { - return currentMime; - } + private void setStatusBarStyle(final Window window) { + String themeType = theme.getType(); + View decorView = window.getDecorView(); + int uiOptions; + int lightStatusBar; - String mime = null; - if (uri != null) { - mime = context.getContentResolver().getType(uri); - } + if (SDK_INT <= 30) { + uiOptions = getDeprecatedSystemUiVisibility(decorView); + lightStatusBar = deprecatedFlagUiLightStatusBar(); - if ((mime == null || mime.isEmpty()) && filename != null) { - mime = getMimeTypeFromExtension(filename); + 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)) + ); + } + + 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 { + 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; + } - if ((mime == null || mime.isEmpty()) && uri != null) { - String path = uri.getPath(); - if (path != null) { - mime = getMimeTypeFromExtension(path); - } - } + private Uri getContentProviderUri(String fileUri) { + return this.getContentProviderUri(fileUri, ""); + } - return (mime != null && !mime.isEmpty()) ? mime : "*/*"; + private Uri getContentProviderUri(String fileUri, String filename) { + if (fileUri == null || fileUri.isEmpty()) { + return null; } - 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); - } + Uri uri = Uri.parse(fileUri); + if (uri == null) { + return null; + } - if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) { - fileProviderAuthority = context.getPackageName() + ".provider"; - } + 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; + } - return fileProviderAuthority; - } + String authority = getFileProviderAuthority(); + if (authority == null) { + Log.e("System", "No FileProvider authority available."); + return null; + } - private boolean isPackageInstalled( - String packageName, - PackageManager packageManager, - CallbackContext callback - ) { + try { + return FileProvider.getUriForFile(context, authority, originalFile); + } catch (IllegalArgumentException | SecurityException ex) { try { - packageManager.getPackageInfo(packageName, 0); - return true; - } catch (PackageManager.NameNotFoundException e) { - return false; + 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 getGlobalSetting(String setting, CallbackContext callback) { - int value = (int) Global.getFloat( - context.getContentResolver(), - setting, -1 - ); - callback.success(value); + 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 void clearCache(CallbackContext callback) { - webView.clearCache(true); - callback.success("Cache cleared"); + 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"; } - 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); + File target = new File(cacheRoot, displayName); + target = ensureUniqueFile(target); + copyFile(source, target); + return target; + } + + private File ensureUniqueFile(File target) { + if (!target.exists()) { + return target; } - private void setNativeContextMenuDisabled(boolean disabled) { - if (webView == null) { - return; - } - webView.setNativeContextMenuDisabled(disabled); + 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 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()); - } + 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(); + } + } + + 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()); + } + } } From d25db1a8809baf1ad509b94fcafba9fbe423a288 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Sun, 5 Jul 2026 14:15:09 +0530 Subject: [PATCH 2/7] feat: fix cordova http --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c2a0cd908..9a74b61ac 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "com.foxdebug.acode.rk.customtabs": {}, "cordova-plugin-system": {}, "cordova-plugin-crashhandler": {}, - "cordova-plugin-advanced-http": {} + "cordova-plugin-advanced-http": { + "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" + } }, "platforms": [ "android" From f8d82ae3a96e2ff982649141a99303e0699e7bc7 Mon Sep 17 00:00:00 2001 From: Rohit Kushvaha Date: Sun, 5 Jul 2026 14:41:44 +0530 Subject: [PATCH 3/7] Update src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../src/android/CrashActivity.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java index 240d89f83..730714c21 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java @@ -218,9 +218,11 @@ public void onClick(View v) { if (restartIntent != null) { restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(restartIntent); + finish(); + System.exit(0); + } else { + Toast.makeText(CrashActivity.this, "Unable to restart the app.", Toast.LENGTH_SHORT).show(); } - finish(); - System.exit(0); } }); buttonsLayout.addView(btnRestart); From 824af6fb56e4caa3974f3f2a3ea29c930b70859c Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Sun, 5 Jul 2026 14:49:22 +0530 Subject: [PATCH 4/7] fix: quality issues --- .../src/android/CrashActivity.java | 444 +++++++++++++++--- .../src/android/CrashHandler.java | 4 +- .../android/com/foxdebug/system/System.java | 37 -- 3 files changed, 385 insertions(+), 100 deletions(-) diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java index 730714c21..3c7235ff0 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java @@ -27,39 +27,253 @@ public class CrashActivity extends Activity { - private String errorType; - private String errorMessage; - private String stackTrace; - private String fullReport; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Retrieve data from intent - 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."; - - // Build system information - 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 + private String errorType; + private String errorMessage; + private String stackTrace; + private String fullReport; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Retrieve data from intent + 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."; + + // Build system information + 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 timestamp = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss", + Locale.getDefault() + ).format(new Date()); + + // Construct full report for copying + fullReport = + "Acode Crash Report\n" + + "==================\n" + + "Time: " + + timestamp + + "\n" + + "Error Type: " + + errorType + + "\n" + + "Error Message: " + + errorMessage + + "\n" + + "App Version: " + + appVersion + + " (" + + appBuild + + ")\n" + + "Device: " + + deviceName + + "\n" + + "Android Version: " + + androidVersion + + "\n\n" + + "Stack Trace:\n" + + stackTrace; + + // --- Build UI Programmatically (Acode Theme Integration) --- + // Main Container ScrollView + ScrollView mainScrollView = new ScrollView(this); + mainScrollView.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ); + mainScrollView.setBackgroundColor(Color.parseColor("#23272a")); // Acode Primary Dark BG + mainScrollView.setFillViewport(true); + + // Vertical Content Container + 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); + + // Header Warning Title + TextView titleView = new TextView(this); + titleView.setText("Acode Crashed"); + titleView.setTextSize(24); + titleView.setTextColor(Color.parseColor("#f5f5f5")); // Acode Primary Text + 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); + + // Explanation text + 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(Color.parseColor("#e4e4e4")); // Acode Secondary Text + 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); + + // --- System Metadata Section --- + TextView metaTitleView = new TextView(this); + metaTitleView.setText("DEVICE & APP INFO"); + metaTitleView.setTextSize(11); + metaTitleView.setTextColor(Color.parseColor("#8ab4f8")); // Acode Link Text color (light blue) + 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(Color.parseColor("#2d3134")); // Acode Secondary Panel BG + cardBg.setCornerRadius(dp(4)); // Acode standard border radius + cardBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border color + 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("Time", timestamp)); + metaCard.addView(createMetaRow("Error Type", errorType)); + rootLayout.addView(metaCard); + + // --- Stack Trace Section --- + TextView logsTitleView = new TextView(this); + logsTitleView.setText("STACK TRACE"); + logsTitleView.setTextSize(11); + logsTitleView.setTextColor(Color.parseColor("#8ab4f8")); + 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); + + // Trace card + 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(Color.parseColor("#181a1f")); // Monospace editor background + traceBg.setCornerRadius(dp(4)); + traceBg.setStroke(dp(1), Color.parseColor("#3a3e46")); + traceCard.setBackground(traceBg); + + LinearLayout.LayoutParams traceCardParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + dp(280) + ); // Fixed height box for logs + 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(Color.parseColor("#e4e4e4")); // Clean light-grey code text + traceView.setTypeface(Typeface.MONOSPACE); + traceView.setHorizontallyScrolling(true); + traceView.setTextIsSelectable(true); + + traceHorizontalScroll.addView(traceView); + traceVerticalScroll.addView(traceHorizontalScroll); + traceCard.addView(traceVerticalScroll); + rootLayout.addView(traceCard); + + // --- Buttons Section (Clean Acode style) --- + 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); + + // Restart Button (Primary Action - Acode Blue theme color) + TextView btnRestart = createButton( + "Restart Acode", + "#ffffff", + "#4285f4", + 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); +<<<<<<< HEAD String deviceName = Build.MANUFACTURER + " " + Build.MODEL; String androidVersion = Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ")"; String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); @@ -304,37 +518,145 @@ private TextView createButton(String text, String textColorHex, String bgHex, bo if (hasBorder) { normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style +======= + // Copy Button (Secondary Action - Flat dark panel with border) + TextView btnCopy = createButton( + "Copy Error Details", + "#e4e4e4", + "#2d3134", + 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(); +>>>>>>> 69787ca3 (fix: quality issues) } - - 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); - - // Tactile touch animations - 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; + } + ); + buttonsLayout.addView(btnCopy); + + // Close Button (Tertiary Action - Flat dark panel with border) + TextView btnClose = createButton("Close", "#e4e4e4", "#2d3134", 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 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(Color.parseColor("#a6accd")); + 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(Color.parseColor("#eeffff")); + 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, + String textColorHex, + String bgHex, + boolean hasBorder + ) { + final TextView btn = new TextView(this); + btn.setText(text); + btn.setTextSize(15); + btn.setTextColor(Color.parseColor(textColorHex)); + 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(Color.parseColor(bgHex)); + normalBg.setCornerRadius(dp(4)); // Standard Acode popup radius + + if (hasBorder) { + normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style } - private int dp(float value) { - return (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - value, - getResources().getDisplayMetrics() - ); - } + 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); + + // Tactile touch animations + 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 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 index d9afa2818..594d53d3f 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java @@ -46,8 +46,8 @@ public void uncaughtException(Thread thread, Throwable ex) { 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); + 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 4362b07f0..124ceba80 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -6,34 +6,16 @@ import android.app.Activity; import android.app.PendingIntent; import android.content.*; -import android.content.ClipData; -import android.content.Context; -import android.content.Context; -import android.content.Context; -import android.content.Intent; -import android.content.Intent; -import android.content.Intent; import android.content.pm.*; -import android.content.pm.InstallSourceInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ImageDecoder; -import android.graphics.ImageDecoder; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Typeface; import android.net.Uri; -import android.net.Uri; -import android.net.Uri; -import android.os.Build; -import android.os.Build; -import android.os.Build; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -56,26 +38,16 @@ import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; -// DocumentFile import (AndroidX library) import androidx.documentfile.provider.DocumentFile; import com.foxdebug.system.Ui.Theme; import java.io.BufferedReader; import java.io.File; -import java.io.File; -// Java I/O imports -import java.io.File; import java.io.FileInputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.IOException; -import java.io.IOException; -import java.io.InputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; @@ -84,27 +56,18 @@ import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.file.Files; -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.security.MessageDigest; import java.util.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.apache.cordova.CallbackContext; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.json.JSONArray; -import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONException; import org.json.JSONObject; From a612e9ce77fd7329e4daa55840f6f5cac6c4f9d9 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Sun, 5 Jul 2026 14:59:09 +0530 Subject: [PATCH 5/7] fix: conflicts --- .../src/android/CrashActivity.java | 450 +++--------------- 1 file changed, 63 insertions(+), 387 deletions(-) diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java index 3c7235ff0..240d89f83 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java @@ -27,253 +27,39 @@ public class CrashActivity extends Activity { - private String errorType; - private String errorMessage; - private String stackTrace; - private String fullReport; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Retrieve data from intent - 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."; - - // Build system information - 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 timestamp = new SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss", - Locale.getDefault() - ).format(new Date()); - - // Construct full report for copying - fullReport = - "Acode Crash Report\n" + - "==================\n" + - "Time: " + - timestamp + - "\n" + - "Error Type: " + - errorType + - "\n" + - "Error Message: " + - errorMessage + - "\n" + - "App Version: " + - appVersion + - " (" + - appBuild + - ")\n" + - "Device: " + - deviceName + - "\n" + - "Android Version: " + - androidVersion + - "\n\n" + - "Stack Trace:\n" + - stackTrace; - - // --- Build UI Programmatically (Acode Theme Integration) --- - // Main Container ScrollView - ScrollView mainScrollView = new ScrollView(this); - mainScrollView.setLayoutParams( - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - ); - mainScrollView.setBackgroundColor(Color.parseColor("#23272a")); // Acode Primary Dark BG - mainScrollView.setFillViewport(true); - - // Vertical Content Container - 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); - - // Header Warning Title - TextView titleView = new TextView(this); - titleView.setText("Acode Crashed"); - titleView.setTextSize(24); - titleView.setTextColor(Color.parseColor("#f5f5f5")); // Acode Primary Text - 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); - - // Explanation text - 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(Color.parseColor("#e4e4e4")); // Acode Secondary Text - 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); - - // --- System Metadata Section --- - TextView metaTitleView = new TextView(this); - metaTitleView.setText("DEVICE & APP INFO"); - metaTitleView.setTextSize(11); - metaTitleView.setTextColor(Color.parseColor("#8ab4f8")); // Acode Link Text color (light blue) - 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(Color.parseColor("#2d3134")); // Acode Secondary Panel BG - cardBg.setCornerRadius(dp(4)); // Acode standard border radius - cardBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border color - 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("Time", timestamp)); - metaCard.addView(createMetaRow("Error Type", errorType)); - rootLayout.addView(metaCard); - - // --- Stack Trace Section --- - TextView logsTitleView = new TextView(this); - logsTitleView.setText("STACK TRACE"); - logsTitleView.setTextSize(11); - logsTitleView.setTextColor(Color.parseColor("#8ab4f8")); - 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); - - // Trace card - 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(Color.parseColor("#181a1f")); // Monospace editor background - traceBg.setCornerRadius(dp(4)); - traceBg.setStroke(dp(1), Color.parseColor("#3a3e46")); - traceCard.setBackground(traceBg); - - LinearLayout.LayoutParams traceCardParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - dp(280) - ); // Fixed height box for logs - 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(Color.parseColor("#e4e4e4")); // Clean light-grey code text - traceView.setTypeface(Typeface.MONOSPACE); - traceView.setHorizontallyScrolling(true); - traceView.setTextIsSelectable(true); - - traceHorizontalScroll.addView(traceView); - traceVerticalScroll.addView(traceHorizontalScroll); - traceCard.addView(traceVerticalScroll); - rootLayout.addView(traceCard); - - // --- Buttons Section (Clean Acode style) --- - 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); - - // Restart Button (Primary Action - Acode Blue theme color) - TextView btnRestart = createButton( - "Restart Acode", - "#ffffff", - "#4285f4", - 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); + private String errorType; + private String errorMessage; + private String stackTrace; + private String fullReport; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Retrieve data from intent + 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."; + + // Build system information + 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 } - } - ); - buttonsLayout.addView(btnRestart); -<<<<<<< HEAD String deviceName = Build.MANUFACTURER + " " + Build.MODEL; String androidVersion = Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ")"; String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); @@ -432,11 +218,9 @@ public void onClick(View v) { if (restartIntent != null) { restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(restartIntent); - finish(); - System.exit(0); - } else { - Toast.makeText(CrashActivity.this, "Unable to restart the app.", Toast.LENGTH_SHORT).show(); } + finish(); + System.exit(0); } }); buttonsLayout.addView(btnRestart); @@ -518,145 +302,37 @@ private TextView createButton(String text, String textColorHex, String bgHex, bo if (hasBorder) { normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style -======= - // Copy Button (Secondary Action - Flat dark panel with border) - TextView btnCopy = createButton( - "Copy Error Details", - "#e4e4e4", - "#2d3134", - 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(); ->>>>>>> 69787ca3 (fix: quality issues) } - } - ); - buttonsLayout.addView(btnCopy); - - // Close Button (Tertiary Action - Flat dark panel with border) - TextView btnClose = createButton("Close", "#e4e4e4", "#2d3134", 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 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(Color.parseColor("#a6accd")); - 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(Color.parseColor("#eeffff")); - 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, - String textColorHex, - String bgHex, - boolean hasBorder - ) { - final TextView btn = new TextView(this); - btn.setText(text); - btn.setTextSize(15); - btn.setTextColor(Color.parseColor(textColorHex)); - 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(Color.parseColor(bgHex)); - normalBg.setCornerRadius(dp(4)); // Standard Acode popup radius - - if (hasBorder) { - normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style + + 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); + + // Tactile touch animations + 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; } - 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); - - // Tactile touch animations - 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 int dp(float value) { - return (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - value, - getResources().getDisplayMetrics() - ); - } + private int dp(float value) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + value, + getResources().getDisplayMetrics() + ); + } } From dc8d2e177e17f1283a425110bbdd9c682c5b5de0 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:14:39 +0530 Subject: [PATCH 6/7] feat(crash-handler): sync crash UI colors with app theme --- .../src/android/CrashActivity.java | 190 ++++++++++++++---- .../android/com/foxdebug/system/System.java | 15 ++ 2 files changed, 167 insertions(+), 38 deletions(-) diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java index 240d89f83..bceace53e 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashActivity.java @@ -5,6 +5,7 @@ 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; @@ -16,6 +17,7 @@ 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; @@ -32,11 +34,28 @@ public class CrashActivity extends Activity { 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); - // Retrieve data from intent + loadThemeColors(); + applySystemBarColors(); + Intent intent = getIntent(); errorType = intent.getStringExtra("error_type"); if (errorType == null) errorType = "Unexpected Crash"; @@ -45,7 +64,6 @@ protected void onCreate(Bundle savedInstanceState) { stackTrace = intent.getStringExtra("stack_trace"); if (stackTrace == null) stackTrace = "No stack trace details available."; - // Build system information String appVersion = "Unknown"; String appBuild = "Unknown"; try { @@ -64,7 +82,6 @@ protected void onCreate(Bundle savedInstanceState) { String androidVersion = Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ")"; String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); - // Construct full report for copying fullReport = "Acode Crash Report\n" + "==================\n" + "Time: " + timestamp + "\n" + @@ -76,16 +93,13 @@ protected void onCreate(Bundle savedInstanceState) { "Stack Trace:\n" + stackTrace; - // --- Build UI Programmatically (Acode Theme Integration) --- - // Main Container ScrollView ScrollView mainScrollView = new ScrollView(this); mainScrollView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mainScrollView.setBackgroundColor(Color.parseColor("#23272a")); // Acode Primary Dark BG + mainScrollView.setBackgroundColor(colorPrimaryBg); mainScrollView.setFillViewport(true); - // Vertical Content Container LinearLayout rootLayout = new LinearLayout(this); rootLayout.setOrientation(LinearLayout.VERTICAL); LinearLayout.LayoutParams rootParams = new LinearLayout.LayoutParams( @@ -95,11 +109,10 @@ protected void onCreate(Bundle savedInstanceState) { rootLayout.setPadding(padding, padding, padding, padding); rootLayout.setLayoutParams(rootParams); - // Header Warning Title TextView titleView = new TextView(this); titleView.setText("Acode Crashed"); titleView.setTextSize(24); - titleView.setTextColor(Color.parseColor("#f5f5f5")); // Acode Primary Text + titleView.setTextColor(colorPrimaryText); titleView.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD)); LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -108,11 +121,10 @@ protected void onCreate(Bundle savedInstanceState) { titleView.setLayoutParams(titleParams); rootLayout.addView(titleView); - // Explanation text 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(Color.parseColor("#e4e4e4")); // Acode Secondary Text + descView.setTextColor(colorSecondaryText); LinearLayout.LayoutParams descParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -120,11 +132,10 @@ protected void onCreate(Bundle savedInstanceState) { descView.setLayoutParams(descParams); rootLayout.addView(descView); - // --- System Metadata Section --- TextView metaTitleView = new TextView(this); metaTitleView.setText("DEVICE & APP INFO"); metaTitleView.setTextSize(11); - metaTitleView.setTextColor(Color.parseColor("#8ab4f8")); // Acode Link Text color (light blue) + metaTitleView.setTextColor(colorLinkText); metaTitleView.setTypeface(Typeface.create("sans-serif", Typeface.BOLD)); LinearLayout.LayoutParams metaTitleParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -138,9 +149,9 @@ protected void onCreate(Bundle savedInstanceState) { metaCard.setPadding(dp(16), dp(16), dp(16), dp(16)); GradientDrawable cardBg = new GradientDrawable(); - cardBg.setColor(Color.parseColor("#2d3134")); // Acode Secondary Panel BG - cardBg.setCornerRadius(dp(4)); // Acode standard border radius - cardBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border color + cardBg.setColor(colorSecondaryBg); + cardBg.setCornerRadius(dp(4)); + cardBg.setStroke(dp(1), colorBorder); metaCard.setBackground(cardBg); LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams( @@ -156,11 +167,10 @@ protected void onCreate(Bundle savedInstanceState) { metaCard.addView(createMetaRow("Error Type", errorType)); rootLayout.addView(metaCard); - // --- Stack Trace Section --- TextView logsTitleView = new TextView(this); logsTitleView.setText("STACK TRACE"); logsTitleView.setTextSize(11); - logsTitleView.setTextColor(Color.parseColor("#8ab4f8")); + logsTitleView.setTextColor(colorLinkText); logsTitleView.setTypeface(Typeface.create("sans-serif", Typeface.BOLD)); LinearLayout.LayoutParams logsTitleParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -169,19 +179,18 @@ protected void onCreate(Bundle savedInstanceState) { logsTitleView.setLayoutParams(logsTitleParams); rootLayout.addView(logsTitleView); - // Trace card 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(Color.parseColor("#181a1f")); // Monospace editor background + traceBg.setColor(colorTraceBg); traceBg.setCornerRadius(dp(4)); - traceBg.setStroke(dp(1), Color.parseColor("#3a3e46")); + traceBg.setStroke(dp(1), colorBorder); traceCard.setBackground(traceBg); LinearLayout.LayoutParams traceCardParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, - dp(280)); // Fixed height box for logs + dp(280)); traceCardParams.setMargins(0, 0, 0, dp(24)); traceCard.setLayoutParams(traceCardParams); @@ -191,7 +200,7 @@ protected void onCreate(Bundle savedInstanceState) { TextView traceView = new TextView(this); traceView.setText(stackTrace); traceView.setTextSize(12); - traceView.setTextColor(Color.parseColor("#e4e4e4")); // Clean light-grey code text + traceView.setTextColor(colorSecondaryText); traceView.setTypeface(Typeface.MONOSPACE); traceView.setHorizontallyScrolling(true); traceView.setTextIsSelectable(true); @@ -201,7 +210,6 @@ protected void onCreate(Bundle savedInstanceState) { traceCard.addView(traceVerticalScroll); rootLayout.addView(traceCard); - // --- Buttons Section (Clean Acode style) --- LinearLayout buttonsLayout = new LinearLayout(this); buttonsLayout.setOrientation(LinearLayout.VERTICAL); LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams( @@ -209,8 +217,7 @@ protected void onCreate(Bundle savedInstanceState) { LinearLayout.LayoutParams.WRAP_CONTENT); buttonsLayout.setLayoutParams(buttonsParams); - // Restart Button (Primary Action - Acode Blue theme color) - TextView btnRestart = createButton("Restart Acode", "#ffffff", "#4285f4", false); + TextView btnRestart = createButton("Restart Acode", colorButtonPrimaryText, colorButtonPrimaryBg, false); btnRestart.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -225,8 +232,7 @@ public void onClick(View v) { }); buttonsLayout.addView(btnRestart); - // Copy Button (Secondary Action - Flat dark panel with border) - TextView btnCopy = createButton("Copy Error Details", "#e4e4e4", "#2d3134", true); + TextView btnCopy = createButton("Copy Error Details", colorButtonSecondaryText, colorButtonSecondaryBg, true); btnCopy.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -238,8 +244,7 @@ public void onClick(View v) { }); buttonsLayout.addView(btnCopy); - // Close Button (Tertiary Action - Flat dark panel with border) - TextView btnClose = createButton("Close", "#e4e4e4", "#2d3134", true); + TextView btnClose = createButton("Close", colorButtonSecondaryText, colorButtonSecondaryBg, true); btnClose.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -254,6 +259,116 @@ public void onClick(View v) { 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); @@ -266,7 +381,7 @@ private View createMetaRow(String label, String value) { TextView labelView = new TextView(this); labelView.setText(label + ": "); labelView.setTextSize(13); - labelView.setTextColor(Color.parseColor("#a6accd")); + labelView.setTextColor(colorMetaLabel); labelView.setTypeface(Typeface.DEFAULT_BOLD); LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams( dp(100), @@ -276,7 +391,7 @@ private View createMetaRow(String label, String value) { TextView valView = new TextView(this); valView.setText(value); valView.setTextSize(13); - valView.setTextColor(Color.parseColor("#eeffff")); + valView.setTextColor(colorMetaValue); LinearLayout.LayoutParams valParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -287,21 +402,21 @@ private View createMetaRow(String label, String value) { return row; } - private TextView createButton(String text, String textColorHex, String bgHex, boolean hasBorder) { + 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(Color.parseColor(textColorHex)); + 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(Color.parseColor(bgHex)); - normalBg.setCornerRadius(dp(4)); // Standard Acode popup radius + normalBg.setColor(bgColor); + normalBg.setCornerRadius(dp(4)); if (hasBorder) { - normalBg.setStroke(dp(1), Color.parseColor("#3a3e46")); // Acode border style + normalBg.setStroke(dp(1), colorBorder); } btn.setBackground(normalBg); @@ -312,7 +427,6 @@ private TextView createButton(String text, String textColorHex, String bgHex, bo btnParams.setMargins(0, 0, 0, dp(12)); btn.setLayoutParams(btnParams); - // Tactile touch animations btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java index 124ceba80..c97ef0c77 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -1718,6 +1718,21 @@ private void setUiTheme( webView.getPluginManager().postMessage("updateSystemBars", null); applySystemBarTheme(); + if (scheme != null) { + try { + 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); From deb6a110249382a586649bc779780e153e8902cb Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:22:08 +0530 Subject: [PATCH 7/7] catch Throwable instead of Exception to prevent silent failures --- .../cordova-plugin-crashhandler/src/android/CrashHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java index 594d53d3f..611741ea0 100644 --- a/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java +++ b/src/plugins/cordova-plugin-crashhandler/src/android/CrashHandler.java @@ -42,7 +42,7 @@ public void uncaughtException(Thread thread, Throwable ex) { Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK ); context.startActivity(intent); - } catch (Exception e) { + } catch (Throwable e) { Log.e(TAG, "Failed to launch CrashActivity", e); } finally { //Should we terminate the app? or let it run with faulty state?