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/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/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/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); 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 { 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