From 6d2ddfff4d31c54b2e47fb6fd31b41396bd7940e Mon Sep 17 00:00:00 2001 From: Afton-programmer Date: Tue, 20 Oct 2026 01:43:44 +0330 Subject: [PATCH 1/3] fix(onboarding): request storage permission only on explicit user tap The welcome page automatically fired the all-files-access settings screen, the runtime media dialogs and the notification dialog as soon as the page appeared. Permissions must only be requested when truly needed (AGENTS.md section 6), so every permission now stays behind its row's Grant button, the same pattern the notification row already used. - Remove autoRequestMissing() and its call from onViewCreated - Files row: on Android 11+ open the all-files settings screen on tap; on Android 8-10 fall back to the runtime storage dialog (the button previously did nothing on those versions) - Catch ActivityNotFoundException on the two settings intents and show a toast instead of crashing on devices without those settings pages - Remove the unused PermissionUtils.requestPermissions(Activity) helper that silently opened the all-files settings screen - Clear the view binding in onDestroyView and guard refreshGranted() --- .../ghostide/onboarding/WelcomeFragment.java | 70 +++++++++++-------- .../ghostide/utils/PermissionUtils.java | 24 ------- app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 41 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/WelcomeFragment.java b/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/WelcomeFragment.java index 5ba6e5f..347b398 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/WelcomeFragment.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/WelcomeFragment.java @@ -1,6 +1,7 @@ package ir.hanzodev1375.ghostide.onboarding; import android.Manifest; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; @@ -19,21 +20,20 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import com.google.android.material.card.MaterialCardView; +import ir.hanzodev1375.components.views.GhostToast; import ir.hanzodev1375.ghostide.R; import ir.hanzodev1375.ghostide.databinding.FragmentWelcomeBinding; import ir.hanzodev1375.ghostide.databinding.ItemOnboardingPermissionBinding; import ir.hanzodev1375.ghostide.utils.PermissionUtils; import ir.theme.M3Theme; -import java.util.ArrayList; -import java.util.List; public class WelcomeFragment extends Fragment { private static final int REQ_NOTIFICATION = 2030; - private static final int REQ_RUNTIME = 1001; private FragmentWelcomeBinding binding; private ActivityResultLauncher storageLauncher; + private ActivityResultLauncher mediaLauncher; @Nullable @Override @@ -72,29 +72,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat storageLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> refreshGranted()); + mediaLauncher = + registerForActivityResult( + new ActivityResultContracts.RequestMultiplePermissions(), + result -> refreshGranted()); refreshGranted(); - autoRequestMissing(); - } - - /** اولین باری که این صفحه باز می‌شود، هر مجوزِ نگرفته را خودکار درخواست می‌کند؛ دکمه‌های - * پایینش برای گرفتن دستی/دوباره باقی می‌مانند. */ - private void autoRequestMissing() { - if (!PermissionUtils.hasManageStoragePermission(requireContext())) { - requestFiles(); - } - if (!PermissionUtils.hasPermissions(requireContext())) { - List missing = new ArrayList<>(); - for (String perm : PermissionUtils.getRequiredPermissions()) { - if (ContextCompat.checkSelfPermission(requireContext(), perm) - != PackageManager.PERMISSION_GRANTED) { - missing.add(perm); - } - } - if (!missing.isEmpty()) { - requestPermissions(missing.toArray(new String[0]), REQ_RUNTIME); - } - } - requestNotifications(); } @Override @@ -106,6 +88,12 @@ public void onResume() { } } + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + private void animateIn() { OnboardingAnim.scaleIn(binding.cardIcon, 0); OnboardingAnim.slideUpFade(binding.tvHeader, 150); @@ -143,6 +131,7 @@ private void bindRow( } private void refreshGranted() { + if (binding == null) return; int fallback = 0xFF202020; setGranted(binding.rowFiles, PermissionUtils.hasManageStoragePermission(requireContext())); boolean notified = @@ -166,9 +155,17 @@ private void setGranted(ItemOnboardingPermissionBinding row, boolean granted) { private void requestFiles() { if (PermissionUtils.hasManageStoragePermission(requireContext())) return; - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - intent.setData(Uri.parse("package:" + requireContext().getPackageName())); - storageLauncher.launch(intent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse("package:" + requireContext().getPackageName())); + try { + storageLauncher.launch(intent); + } catch (ActivityNotFoundException e) { + showPermissionSettingsMissing(); + } + } else { + mediaLauncher.launch(PermissionUtils.getRequiredPermissions()); + } } private void requestNotifications() { @@ -181,15 +178,26 @@ private void requestNotifications() { private void requestInstallApps() { Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); intent.setData(Uri.parse("package:" + requireContext().getPackageName())); - startActivity(intent); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + showPermissionSettingsMissing(); + } + } + + private void showPermissionSettingsMissing() { + GhostToast.makeText( + requireContext(), + getString(R.string.onboarding_perm_unavailable), + GhostToast.LENGTH_SHORT) + .show(); } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if ((requestCode == REQ_NOTIFICATION || requestCode == REQ_RUNTIME) && binding != null) - refreshGranted(); + if (requestCode == REQ_NOTIFICATION && binding != null) refreshGranted(); } private static void tintCard(MaterialCardView card, int color) { diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/utils/PermissionUtils.java b/app/src/main/java/ir/hanzodev1375/ghostide/utils/PermissionUtils.java index 174d2dc..0358222 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/utils/PermissionUtils.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/utils/PermissionUtils.java @@ -12,14 +12,9 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; -import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import java.util.ArrayList; -import java.util.List; - public class PermissionUtils { - public static final int REQUEST_CODE = 1001; // برای اندروید 15 نیاز به مجوز مدیریت فایل کامل public static String[] getRequiredPermissions() { @@ -77,23 +72,4 @@ public static boolean hasPermissions(Context context) { return true; } - - public static void requestPermissions(Activity activity) { - List missing = new ArrayList<>(); - for (String perm : getRequiredPermissions()) { - if (ContextCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) - missing.add(perm); - } - - if (!missing.isEmpty()) { - ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), REQUEST_CODE); - } - - // درخواست مجوز مدیریت فایل - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - intent.setData(Uri.parse("package:" + activity.getPackageName())); - activity.startActivityForResult(intent, REQUEST_CODE + 1); - } - } } diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 76ef2a7..45d704f 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -609,6 +609,7 @@ نصب بسته های برنامه را مجاز کنید گرانت داده شده است + این تنظیم روی دستگاه شما در دسترس نیست یک ترمینال واقعی لینوکس در جیب شما به شما امکان می دهد بسته ها را نصب کرده و ابزارها را اجرا کنید. بدون آن برخی از ویژگی ها و دستورات LSP کار نمی کنند. برخی از LSP ها بدون ترمینال کار نمی کنند برای نصب کلیک کنید diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b5c0ad..d9774f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -612,6 +612,7 @@ Allow installing app packages Grant Granted + This setting is not available on your device A real Linux terminal in your pocket lets you install packages and run tools. Without it some LSP features and commands will not work. Some LSPs won\'t work without the terminal Click to install From 0c66e255bf381a2444fc002445b346116b24d86d Mon Sep 17 00:00:00 2001 From: Afton-programmer Date: Tue, 20 Oct 2026 01:43:45 +0330 Subject: [PATCH 2/3] fix(app): initialize plugin host at startup and fix activity leaks - Initialize PluginManager in GhostIdeAppLoader so the file manager can register with plugins on first launch; previously the registration NPE was swallowed until EditorActivity had been opened once - Drop PluginManager's write-only activity fields (static leaks) - Add clearEditorActivityReferences()/clearFileManagerActivityReferences() and call them from EditorActivity/FileManagerActivity onDestroy so plugins no longer retain destroyed activities - Cancel the delayed plugin-host Runnable on FileManagerActivity destroy - Reorder FileManagerActivity.onDestroy so the host registration, git helper and executor are always disposed; guard receiver unregistration and the music preview against a missing binding - Extract cursor.zip off the main thread with try-with-resources, reject zip entries that escape the target directory, and log failures instead of ignoring them --- .../ghostide/GhostIdeAppLoader.java | 71 ++++++++++++------- .../ghostide/activity/EditorActivity.java | 7 +- .../activity/FileManagerActivity.java | 47 ++++++++---- .../ghostide/plugin/PluginManager.java | 25 ++++++- 4 files changed, 104 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/GhostIdeAppLoader.java b/app/src/main/java/ir/hanzodev1375/ghostide/GhostIdeAppLoader.java index 7cd2d43..7419e87 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/GhostIdeAppLoader.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/GhostIdeAppLoader.java @@ -26,6 +26,7 @@ import ir.hanzodev1375.ghostide.plugin.BackgroundRendererBridge; import ir.hanzodev1375.ghostide.plugin.api.ExtensionRegistration; import ir.hanzodev1375.ghostide.plugin.api.GlobalRegistry; +import ir.hanzodev1375.ghostide.plugin.PluginManager; import ir.hanzodev1375.ghostide.plugin.gpl.GplInstalledPlugins; import ir.hanzodev1375.ghostide.plugin.gpl.GplPluginLoader; import ir.hanzodev1375.ghostide.shizuku.ShizukuManager; @@ -41,6 +42,7 @@ public class GhostIdeAppLoader extends Application { + private static final String TAG = "GhostIdeAppLoader"; private static Context mApplicationContext; private static GhostIdeAppLoader loader; private final StringBuilder softwareInfo = new StringBuilder(); @@ -95,6 +97,8 @@ public void onCreate() { }); BackgroundRendererBridge.install(); GlslBackgroundFactory.register(); + // Initialize the plugin host at app start so the file manager can reach it on first launch. + PluginManager.init(this); GplInstalledPlugins.loadAll(this, GplPluginLoader.getInstance(this)); PluginNotifier.schedule(this); @@ -154,32 +158,45 @@ public ThemeUtils getThemeUtils() { } private void extractCursorZip() { - try { - File cursorDir = new File("/storage/emulated/0/ghostide/cursor"); - File jsonMarker = new File(cursorDir, "cursor.json"); - if (cursorDir.exists() && jsonMarker.exists()) { - return; - } - cursorDir.mkdirs(); - InputStream is = getAssets().open("cursor.zip"); - ZipInputStream zis = new ZipInputStream(is); - ZipEntry entry; - byte[] buffer = new byte[4096]; - while ((entry = zis.getNextEntry()) != null) { - if (entry.isDirectory()) continue; - String name = entry.getName(); - File outFile = new File(cursorDir, name); - FileOutputStream fos = new FileOutputStream(outFile); - int len; - while ((len = zis.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } - fos.close(); - zis.closeEntry(); - } - zis.close(); - is.close(); - } catch (Exception ignored) { - } + // Zip extraction is blocking I/O; editor settings read these files lazily, + // so keep it off the main thread and log failures instead of ignoring them. + new Thread( + () -> { + File cursorDir = new File("/storage/emulated/0/ghostide/cursor"); + File jsonMarker = new File(cursorDir, "cursor.json"); + if (cursorDir.exists() && jsonMarker.exists()) { + return; + } + if (!cursorDir.exists() && !cursorDir.mkdirs()) { + Log.w(TAG, "Cannot create cursor directory: " + cursorDir.getAbsolutePath()); + return; + } + try (InputStream is = getAssets().open("cursor.zip"); + ZipInputStream zis = new ZipInputStream(is)) { + String canonicalDir = cursorDir.getCanonicalPath() + File.separator; + byte[] buffer = new byte[4096]; + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + File outFile = new File(cursorDir, entry.getName()); + // Reject entries that would escape the target directory (zip path traversal). + if (!outFile.getCanonicalPath().startsWith(canonicalDir)) { + Log.w(TAG, "Skipped cursor.zip entry outside target dir: " + entry.getName()); + continue; + } + try (FileOutputStream fos = new FileOutputStream(outFile)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + zis.closeEntry(); + } + } catch (Exception e) { + Log.w(TAG, "Failed to extract cursor.zip", e); + } + }, + "CursorZipExtractor") + .start(); } } diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/activity/EditorActivity.java b/app/src/main/java/ir/hanzodev1375/ghostide/activity/EditorActivity.java index 2193ec3..b635ed9 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/activity/EditorActivity.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/activity/EditorActivity.java @@ -404,7 +404,12 @@ private void togglePinByPath(String filePath) { @Override protected void onDestroy() { super.onDestroy(); - EventBus.getDefault().unregister(this); + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this); + } + if (PluginManager.getInstance() != null) { + PluginManager.getInstance().clearEditorActivityReferences(); + } lspStatusHandler.removeCallbacks(lspStatusPollRunnable); if (keyboardLayoutListener != null) { getWindow() diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/activity/FileManagerActivity.java b/app/src/main/java/ir/hanzodev1375/ghostide/activity/FileManagerActivity.java index 92f012f..b34b87a 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/activity/FileManagerActivity.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/activity/FileManagerActivity.java @@ -149,6 +149,8 @@ public class FileManagerActivity extends BaseCompat private PreferencesUtils appsetting; private ProfileView profileview; private NetworkChangeReceiver networkChangeReceiver; + private Handler pluginHostHandler; + private Runnable pluginHostRunnable; private CopyProgressDialog copyProgressDialog; private DeleteProgressDialog deleteProgressDialog; private HistoryViewModel historyViewModel; @@ -214,16 +216,16 @@ protected void onCreate(Bundle savedInstanceState) { } else { this.registerReceiver(networkChangeReceiver, filter); } - new Handler(Looper.getMainLooper()) - .postDelayed( - () -> { - try { - PluginManager.getInstance().setCurrentFileManagerActivity(this); - } catch (Exception e) { - e.printStackTrace(); - } - }, - 100); + pluginHostHandler = new Handler(Looper.getMainLooper()); + pluginHostRunnable = + () -> { + try { + PluginManager.getInstance().setCurrentFileManagerActivity(this); + } catch (Exception e) { + e.printStackTrace(); + } + }; + pluginHostHandler.postDelayed(pluginHostRunnable, 100); viewModel = new ViewModelProvider(this).get(FileViewModel.class); fileManagerHostRegistration = @@ -1179,15 +1181,30 @@ protected void onDestroy() { thanosEffect.kill(); thanosEffect = null; } - bind.musicPreview.release(); - bind = null; - this.unregisterReceiver(networkChangeReceiver); - gitHelper.shutdown(); - ftpExecutor.shutdownNow(); + if (pluginHostHandler != null) { + pluginHostHandler.removeCallbacks(pluginHostRunnable); + pluginHostHandler = null; + } + if (PluginManager.getInstance() != null) { + PluginManager.getInstance().clearFileManagerActivityReferences(); + } if (fileManagerHostRegistration != null) { fileManagerHostRegistration.dispose(); fileManagerHostRegistration = null; } + gitHelper.shutdown(); + ftpExecutor.shutdownNow(); + if (bind != null) { + bind.musicPreview.release(); + bind = null; + } + if (networkChangeReceiver != null) { + try { + this.unregisterReceiver(networkChangeReceiver); + } catch (IllegalArgumentException e) { + // Receiver was not registered; onCreate aborted before registration. + } + } } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/plugin/PluginManager.java b/app/src/main/java/ir/hanzodev1375/ghostide/plugin/PluginManager.java index 6d2c02d..935e83d 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/plugin/PluginManager.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/plugin/PluginManager.java @@ -13,8 +13,6 @@ public class PluginManager { private static PluginManager instance; private PluginLoader loader; private final Map configs = new HashMap<>(); - private EditorActivity currentEditorActivity; - private FileManagerActivity currentFileManagerActivity; private PluginManager(Context context) { loader = new PluginLoader(context); @@ -42,7 +40,6 @@ public void loadPluginsFromConfig(String configPath) { public void setCurrentEditorActivity( EditorActivity activity, IdeEditor editor, String filePath, String fileExtension) { - this.currentEditorActivity = activity; for (Plugin p : getPluginsForFileType(fileExtension)) { p.setEditorActivity(activity); p.onEditorReady(editor); @@ -63,6 +60,28 @@ public void setCurrentFileManagerActivity(FileManagerActivity activity) { } } + /** Clears cached editor activity references so destroyed activities are not retained. */ + public void clearEditorActivityReferences() { + for (Plugin p : loader.getAllPlugins().values()) { + try { + p.setEditorActivity(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** Clears cached file manager references so destroyed activities are not retained. */ + public void clearFileManagerActivityReferences() { + for (Plugin p : loader.getAllPlugins().values()) { + try { + p.setFileManagerActivity(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + public void notifyFileSaved(String filePath, String fileExtension, String content) { for (Plugin p : getPluginsForFileType(fileExtension)) { p.onFileSaved(filePath, content); From 36f28e563b1423b3e7746cd7ae73f4f704e55c9c Mon Sep 17 00:00:00 2001 From: Afton-programmer Date: Tue, 20 Oct 2026 01:43:46 +0330 Subject: [PATCH 3/3] fix: detach installer listener and guard network receiver callback - OnboardingViewModel now detaches its DebianInstaller listener in onCleared(); DebianInstaller keeps the listener in a static field, so the ViewModel leaked until a new terminal screen attached - NetworkChangeReceiver ignores deliveries when no callback is set; manifest-instantiated receivers would otherwise crash with an NPE --- .../hanzodev1375/ghostide/onboarding/OnboardingViewModel.java | 1 + .../ir/hanzodev1375/ghostide/utils/NetworkChangeReceiver.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/OnboardingViewModel.java b/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/OnboardingViewModel.java index be6d90e..8f6d979 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/OnboardingViewModel.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/onboarding/OnboardingViewModel.java @@ -271,6 +271,7 @@ public boolean isDebianInstalled() { @Override protected void onCleared() { super.onCleared(); + DebianInstaller.detach(installListener); mainHandler.removeCallbacksAndMessages(null); } } \ No newline at end of file diff --git a/app/src/main/java/ir/hanzodev1375/ghostide/utils/NetworkChangeReceiver.java b/app/src/main/java/ir/hanzodev1375/ghostide/utils/NetworkChangeReceiver.java index e7619f5..d56e5b1 100644 --- a/app/src/main/java/ir/hanzodev1375/ghostide/utils/NetworkChangeReceiver.java +++ b/app/src/main/java/ir/hanzodev1375/ghostide/utils/NetworkChangeReceiver.java @@ -16,6 +16,8 @@ public NetworkChangeReceiver(CallBackNetWork callback) { @Override public void onReceive(Context context, Intent intent) { + // Manifest-instantiated receivers get no callback; ignore those deliveries. + if (callback == null) return; if (isOnline(context)) { callback.ConnectionIS(); } else {