diff --git a/.openhands/memory/2026-11-14.md b/.openhands/memory/2026-11-14.md new file mode 100644 index 00000000..b872e509 --- /dev/null +++ b/.openhands/memory/2026-11-14.md @@ -0,0 +1,83 @@ +# 2026-11-14 + +## Task: remove the legacy UI (screens/mod menu/splash/tabs/rail/buttons) for a new UI drop-in + +### Toolchain (this container had none) +Reinstalled per `docs/agents/recovering-the-build-toolchain-in-a-fresh-container.md`: +`/workspace/tools/jdk-21.0.13+9` (Temurin 21.0.13+9) + `/workspace/android-sdk` +(platform-tools, android-35/36, build-tools 35/36, ndk 28.2.13676358, cmake 3.22.1). +`local.properties` -> `sdk.dir=/workspace/android-sdk`. +**`assembleDebug` additionally needs the NESTED submodules** — plain `git submodule update --init` +leaves `libHttpClient/External/zlib` empty and CMake fails at +`libHttpClient.Android/CMakeLists.txt:127` ("Cannot find source file: .../External/zlib/adler32.c", +"No SOURCES given to target: libHttpClient.Android"). Fix: `git submodule update --init --recursive`. + +### Baseline at HEAD 6b83ec5 (green) +`:app:testDebugUnitTest` = 3456 tests, 0 failures, 1 skipped. `:app:assembleDebug` SUCCESSFUL (~4 min). + +### Coupling finding (the crux of this task) +- The manifest lists `org.chimeramc.client.ui.activities.*`, but those files physically live in + `app/src/main/java/org/chimeramc/client/launcher/ui/activities/` — i.e. **the directory tree does + not match the package tree** for the whole `launcher/ui` tree (package decls are + `org.chimeramc.client.ui.*`). Operate on FILES, never assume dir==package. +- `client.ui` + `client.launcher.ui` = 144 files across 21 packages (~40k LOC), 94 layouts. +- **No engine/core package imports the UI**, but **41 kept files reference deleted UI symbols** + (DynamicAnim, CustomAlertDialog, UiTokens, MainViewModel, the Activity classes as Intent targets, + etc.). Those are the rewiring surface. + +## In-game Mod Menu deletion (task 2) +The in-game menu had **three layers**; all three are now cut: +1. **Native ImGui** in the sweetloader submodule: `src/pl/menu/` (MenuTheme/Widgets/Render/Filter/ + Host/Jni, ~1900 LOC). It was self-contained — the ONLY external consumer was + `src/pl/render/PresentFrameEffect.cpp::OnPresentBoundary` (`pl::menu::IsOpen/IsMenuLive/Frame`). + Removed: the six `src/pl/menu/*.cpp` from `CMakeLists.txt`, the `add_library(imgui …)` target + + its `target_link_libraries(sweetloader … imgui)`, the present-hook call, `third_party/imgui/` + (2.8 MB, vendored, only consumer was the menu) and its NOTICE attribution, and + `tests/cosmetics/MenuFilterTest.cpp` + its `run_tests.sh` block. + Submodule gates that must stay green: `scripts/check_sources_listed.py` (CMake sources ↔ + `src/**/*.cpp` ↔ `run_tests.sh`), `scripts/check_pinned_deps.py` (NOTICE ↔ fetched pins; the + vendored imgui block is NOT checked by it, but remove it anyway for honesty), host suite + `bash tests/cosmetics/run_tests.sh`. +2. **Java bridge**: `launcher.preloader.PreloaderMenu` + `core.mods.inbuilt.overlay.InGameMenuController` + (both already deleted). +3. **Launcher overlay widgets** (content panels behind the native menu): deleted + `MoreButtonsEditor`, `VoicePanel`, `VoiceUiKit`, `CosmeticsPanel` + their tests + (`PauseMenuActionsTest`, `QuickSettingsTest`, `ModMenuMetricsTest`, `ModMenuPaletteTest`, + `ModMenuSettingsTest`, `CursorControllerTest`, `CursorRouterTest`, `ModMenuNavLayoutTest`, + `ModUnavailableReasonResourcesTest`). +Rewiring done to keep the systems compiling: +- `MinecraftActivity.kt`: dropped `InGameMenuController.install/release/getInstance().handleTouch/ + drainAndApply` and the `MoreButtonsEditor.onActivityResult/closeEditor/onResume` hooks. +- `InbuiltModuleProvider.java`: `ModIds.MORE_BUTTONS` no longer opens an editor (null action). +- `InbuiltModManager.java`: removed `get/setModMenuSettings` (they referenced the deleted + `ModMenuSettings`). Kept the appearance prefs/keys and `getModMenuButtonOpacity`. +- `InbuiltOverlayManager.java`: removed `modMenuButton`/`pauseMenuRow`/`DebugMenuOverlay`… (DebugMenu + is the F3 overlay MOD, NOT the menu — it stays). + +## Legacy visual-asset cleanup (task 1 tail) +Deleted 29 dead legacy drawables (splash art, launch/dock buttons, nav rail): `splash_bg_*` (8), +`launch_bg_*` (3), `splash_scrim`, `bg_logo_glow`, `bg_launch_button`, `bg_split_launch`, +`bg_mod_menu_fab/toggle`, `bg_vip_icon_button`, `ic_nav_*` (3), `nav_active_indicator`, +`nav_rail_background`, `nav_tab_dot`, `bg_dock_play`, `bg_dock_version_chip`, `bg_launch_dock`, +`bg_launch_quickdock`, `bg_vip_surface`. Also removed the dead `Widget.Caramel.NavTab/NavRailTab/ +QuickDock*` styles (86 lines). Kept 60+ generic drawables + the whole token/style system a new UI +would reuse (bg_*, ic_*, glass_card, Widget.Caramel.Card*, TextAppearance.Caramel.*). +Two tests pinned the removed button drawables — trimmed both (not deleted): `ButtonPressDrawableTest` +(now only `button_accent.xml`) and `RadiusScaleTest`'s surface list (dropped the 3 gone surfaces). + +## TRAP: regex `` removal over-matched +Removing `Widget.Caramel.QuickDock` (a **self-closing** `')` pattern ran PAST it to the next real `` and ate +the `TextAppearance.Caramel` base style + its doc comment. Recovered by restoring the block from +`git show HEAD:…`. Lesson: match self-closing styles explicitly (`/>` vs ``). + +## Final verification (both repos) +- Parent: `:app:testDebugUnitTest` = **3164 tests, 0 failures**, `:app:lintDebug` clean, + `:app:assembleDebug` SUCCESSFUL (clean `app/.cxx` rebuild included, produces + `libsweetloader.so`). `scripts/check_ui_hygiene.py` clean (findViewById baseline 32→29 after + `--update`). Layouts 94→20; drawables ~167→161 distinct names. +- Submodule: host suite passes; `check_sources_listed.py` + `check_pinned_deps.py` green. +- Parent diff: **286 files, +13 / −57496**. Submodule diff: **30 files, −54879**. +- Note: `git status` counts differ from `diff --stat` because many deletions were staged with + `git rm` (staged) while others were plain `rm` (unstaged) — use `git diff --stat HEAD` for the + true total. diff --git a/.openhands/memory/MEMORY.md b/.openhands/memory/MEMORY.md index d9139e7a..1f387287 100644 --- a/.openhands/memory/MEMORY.md +++ b/.openhands/memory/MEMORY.md @@ -10,6 +10,28 @@ Index only. Detail and session notes live in the daily logs (`YYYY-MM-DD.md`). The repo was called **GlowberryClient** before; the package is `org.chimeramc.client` either way. +### UI skeleton state (2026-11-14: the whole legacy UI was deleted for a new one) +The launcher UI + the in-game Mod Menu UI were removed in one sweep, keeping every system. Do not +re-add the old UI; a new one is being dropped in. See the 2026-11-14 daily log for the full list. +- **`ui.activities` files live under `launcher/ui/activities/`** (dir ≠ package for the whole + `launcher/ui` tree — package decls are `org.chimeramc.client.ui.*`). Operate on FILES, not dirs. +- 7 placeholder Activities remain (`BaseActivity` + `MainActivity`/`SplashActivity`/`LibraryActivity`/ + `SettingsActivity`/`ModsFullscreenActivity`/`CrashActivity`); they draw nothing but are manifest + targets. `MainActivity.setTextMinecraftVersion()` and `LibraryActivity.EXTRA_INITIAL_TAB`/`TAB_*` + are kept no-op API other code still calls. Layouts 94→20; the kept 20 are the game-host/HUD ones. +- **The in-game menu was 3 layers, all cut**: native ImGui (`sweetloader/src/pl/menu/` + vendored + `third_party/imgui/`, consumed only by `PresentFrameEffect.cpp`), the Java bridge + (`PreloaderMenu`/`InGameMenuController`), and the launcher overlay widgets (`MoreButtonsEditor`, + `VoicePanel`/`VoiceUiKit`, `CosmeticsPanel`). `DebugMenuOverlay` is the F3 overlay MOD — keep it. +- **Kept (a new UI will reuse them)**: `UiTokens`, `Widget.Caramel.Card*`, `TextAppearance.Caramel.*`, + `GlowDimens`, generic `bg_*`/`ic_*` drawables, `PersonalizationManager`, all `core.*` systems. + Removed only the dead legacy chrome (`splash_bg_*`, `launch_bg_*`, dock/nav drawables + styles). +- Gate that catches dead drawables: `LayoutResourceTest` (unreferenced layout) fails the build — + delete the layout. Hygiene: `scripts/check_ui_hygiene.py --update` retires stale findViewById + baseline rows and shrinks the baseline when a file is deleted. +- **Trap**: a regex `" + - "" + - "" + - result + - ""; - summary.loadDataWithBaseURL("https://www.curseforge.com", htmlData, "text/html", "utf-8", null); - } - - }); - } - - @Override - public void onError(Throwable t) { - } - }); - } - - private void onInstallClick() { - loadAllFiles(); - } - - private void loadAllFiles() { - progressBar.setVisibility(View.VISIBLE); - btnInstall.setEnabled(false); - - client.getModFiles(content.id, 0, 50, new CurseForgeClient.CurseForgeCallback<>() { - @Override - public void onSuccess(org.chimeramc.client.core.curseforge.models.ModFilesResponse result) { - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - if (result != null && result.data != null) { - showFilesDialog(result.data); - } else { - Toast.makeText(ContentDetailsActivity.this, R.string.no_files_found, Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onError(Throwable t) { - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - if (t instanceof CurseForgeClient.MissingApiKeyException) { - org.chimeramc.client.core.curseforge.CurseForgeKeyDialog.show( - ContentDetailsActivity.this, ContentDetailsActivity.this::loadAllFiles); - return; - } - Toast.makeText(ContentDetailsActivity.this, - UserFacingError.reason(ContentDetailsActivity.this, t), - Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void showFilesDialog(java.util.List files) { - com.google.android.material.bottomsheet.BottomSheetDialog dialog = new com.google.android.material.bottomsheet.BottomSheetDialog(this); - dialog.setContentView(R.layout.dialog_file_list); - - org.chimeramc.client.util.PersonalizationManager pm = new org.chimeramc.client.util.PersonalizationManager(this); - int accent = pm.getAccentColor(); - - RecyclerView recycler = dialog.findViewById(R.id.recycler_files); - - org.chimeramc.client.ui.adapter.FileListAdapter adapter = new org.chimeramc.client.ui.adapter.FileListAdapter(file -> { - dialog.dismiss(); - downloadAndImport(file); - }, accent); - - recycler.setLayoutManager(new LinearLayoutManager(this)); - recycler.setAdapter(adapter); - adapter.setFiles(files); - - if (dialog.getWindow() != null) { - dialog.getWindow().setFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - } - - dialog.show(); - - if (dialog.getWindow() != null) { - android.view.View decorView = dialog.getWindow().getDecorView(); - decorView.setSystemUiVisibility( - android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | android.view.View.SYSTEM_UI_FLAG_FULLSCREEN - | android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE - ); - if (ApiLevels.atLeastR()) { - dialog.getWindow().setDecorFitsSystemWindows(false); - android.view.WindowInsetsController controller = decorView.getWindowInsetsController(); - if (controller != null) { - controller.setSystemBarsBehavior(android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - controller.hide(android.view.WindowInsets.Type.statusBars() | android.view.WindowInsets.Type.navigationBars()); - } - } - dialog.getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - } - - dialog.getBehavior().setState(com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED); - } - - private void onBrowserClick() { - if (content.links != null && content.links.websiteUrl != null) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(content.links.websiteUrl)); - startActivity(intent); - } else { - Toast.makeText(this, R.string.no_website_link_available, Toast.LENGTH_SHORT).show(); - } - } - - private void downloadAndImport(ContentFile file) { - runOnUiThread(() -> { - progressBar.setVisibility(View.VISIBLE); - progressBar.setIndeterminate(true); - btnInstall.setEnabled(false); - Toast.makeText(this, getString(R.string.curseforge_downloading), Toast.LENGTH_SHORT).show(); - }); - - if (!runOnDownloadExecutor(() -> { - try { - File cacheDir = getCacheDir(); - File outputFile = new File(cacheDir, file.fileName); - - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - org.chimeramc.client.settings.LowLatencyNetworkManager.configure(clientBuilder); - OkHttpClient client = clientBuilder.build(); - Request request = new Request.Builder().url(file.downloadUrl).build(); - - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) throw new IOException("Failed to download"); - - try (InputStream is = response.body().byteStream(); - FileOutputStream fos = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[8192]; - int read; - while ((read = is.read(buffer)) != -1) { - fos.write(buffer, 0, read); - } - } - } - - runOnUiThread(() -> { - Toast.makeText(this, getString(R.string.curseforge_importing), Toast.LENGTH_SHORT).show(); - importFile(outputFile); - }); - - } catch (Throwable e) { - // Throwable, not Exception: this handler is the only thing that clears the progress - // bar and re-enables Install, and a CurseForge download buffers a whole pack in - // memory - an OutOfMemoryError skipped it and left the screen stuck mid-download. - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - Toast.makeText(this, getString(R.string.curseforge_download_failed) + "\n\n" - + UserFacingError.reason(this, e), Toast.LENGTH_LONG).show(); - }); - } - })) { - // onDestroy shut the download pool down while the Install tap was being handled. - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - } - } - - /** - * Submits a CurseForge download, tolerating a pool {@link #onDestroy()} has shut down. - * - *

Install is a button, so a tap that lands as the screen closes would otherwise throw - * {@link java.util.concurrent.RejectedExecutionException} on the main thread. Returns false when - * the work could not be submitted. - */ - private boolean runOnDownloadExecutor(Runnable work) { - return SafeExecutor.submit(downloadExecutor, work); - } - - private void importFile(File file) { - Uri uri = Uri.fromFile(file); - - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion == null) { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - Toast.makeText(this, R.string.not_found_version, Toast.LENGTH_SHORT).show(); - return; - } - - File worldsDir = getWorldsDirectory(); - File resourcePacksDir = getPackDirectory("resource_packs"); - File behaviorPacksDir = getPackDirectory("behavior_packs"); - File skinPacksDir = getPackDirectory("skin_packs"); - - contentImporter.importContent(java.util.Collections.singletonList(uri), resourcePacksDir, behaviorPacksDir, skinPacksDir, worldsDir, - new ContentImporter.ImportCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - Toast.makeText(ContentDetailsActivity.this, message, Toast.LENGTH_SHORT).show(); - if (file.exists()) file.delete(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - progressBar.setVisibility(View.GONE); - btnInstall.setEnabled(true); - Toast.makeText(ContentDetailsActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) { - } - }); - } - private File getPackDirectory(String packType) { - File gameDataDir = getGameDataDirForSavedStorageType(); - return gameDataDir == null ? null : new File(gameDataDir, packType); - } - - private File getWorldsDirectory() { - File gameDataDir = getGameDataDirForSavedStorageType(); - return gameDataDir == null ? null : new File(gameDataDir, "minecraftWorlds"); - } - - private File getGameDataDirForSavedStorageType() { - android.content.SharedPreferences prefs = getSharedPreferences("content_management", MODE_PRIVATE); - String savedType = prefs.getString("storage_type", "INTERNAL"); - FeatureSettings.StorageType currentStorageType = parseStorageType(savedType); - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion == null) return null; - currentStorageType = LauncherStorage.normalizeContentStorageType( - currentStorageType, - currentVersion.versionIsolation - ); - return LauncherStorage.getContentGameDataDir(this, currentVersion.getStorageProfileId(), currentStorageType); - } - - private FeatureSettings.StorageType parseStorageType(String value) { - try { - return FeatureSettings.StorageType.valueOf(value); - } catch (Exception ignored) { - return FeatureSettings.StorageType.INTERNAL; - } - } - - private String getHexColor(int colorResId) { - int color = getResources().getColor(colorResId, getTheme()); - return String.format("#%06X", (0xFFFFFF & color)); - } -} diff --git a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentListActivity.java b/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentListActivity.java deleted file mode 100644 index 96ccb632..00000000 --- a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentListActivity.java +++ /dev/null @@ -1,1326 +0,0 @@ -package org.chimeramc.client.ui.activities; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.View; -import android.widget.EditText; -import android.widget.RadioButton; -import android.widget.RadioGroup; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.chimeramc.client.R; -import org.chimeramc.client.core.content.ContentManager; -import org.chimeramc.client.core.content.ResourcePackItem; -import org.chimeramc.client.core.content.ResourcePackManager; -import org.chimeramc.client.core.content.RakNetPing; -import org.chimeramc.client.core.content.ServerItem; -import org.chimeramc.client.core.content.ServerPongInfo; -import org.chimeramc.client.core.content.StructureExtractor; -import org.chimeramc.client.core.content.WorldItem; -import org.chimeramc.client.core.content.WorldManager; -import org.chimeramc.client.core.versions.GameVersion; -import org.chimeramc.client.core.versions.VersionManager; -import org.chimeramc.client.databinding.ActivityContentListBinding; -import org.chimeramc.client.settings.FeatureSettings; -import org.chimeramc.client.ui.adapter.ResourcePacksAdapter; -import org.chimeramc.client.ui.adapter.StructuresAdapter; -import org.chimeramc.client.ui.adapter.WorldsAdapter; -import org.chimeramc.client.ui.animation.DynamicAnim; -import org.chimeramc.client.ui.dialogs.CustomAlertDialog; -import org.chimeramc.client.util.LauncherStorage; - -import android.provider.MediaStore; -import android.content.ContentValues; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import java.io.OutputStream; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; -import org.chimeramc.client.util.AppLog; -import org.chimeramc.client.util.AppExecutors; -import org.chimeramc.client.util.MainThread; - -public class ContentListActivity extends BaseActivity { - - private static final String TAG = "ContentListActivity"; - - public static final String EXTRA_CONTENT_TYPE = "content_type"; - public static final String EXTRA_WORLDS_DIRECTORY = "worlds_directory"; - public static final String EXTRA_CURRENT_STORAGE_TYPE = "current_storage_type"; - public static final int TYPE_WORLDS = 0; - public static final int TYPE_SKIN_PACKS = 1; - public static final int TYPE_RESOURCE_PACKS = 2; - public static final int TYPE_BEHAVIOR_PACKS = 3; - public static final int TYPE_SCREENSHOTS = 4; - public static final int TYPE_SERVERS = 5; - - private ActivityContentListBinding binding; - private ContentManager contentManager; - private VersionManager versionManager; - private int contentType; - private File worldsDirectory; - private FeatureSettings.StorageType currentStorageType; - - private WorldsAdapter worldsAdapter; - private ResourcePacksAdapter packsAdapter; - private org.chimeramc.client.ui.adapter.ScreenshotsAdapter screenshotsAdapter; - private org.chimeramc.client.ui.adapter.ServersAdapter serversAdapter; - - private ActivityResultLauncher exportLauncher; - private ActivityResultLauncher exportPackLauncher; - private ActivityResultLauncher customFlatWorldLauncher; - private ActivityResultLauncher structureExportLauncher; - private WorldItem pendingExportWorld; - private ResourcePackItem pendingExportPack; - private WorldItem pendingStructureExportWorld; - private StructureExtractor.StructureInfo pendingStructureInfo; - private StructureExtractor structureExtractor; - - private List allWorlds = new ArrayList<>(); - private List allPacks = new ArrayList<>(); - private List allServers = new ArrayList<>(); - - private org.chimeramc.client.ui.dialogs.LoadingDialog progressDialog; - - private final Handler mainHandler = MainThread.handler(); - private ExecutorService pingExecutor; - /** Server addresses ("ip:port") that already answered; they are not re-pinged every refresh. */ - private final java.util.Set pingedServerKeys = new java.util.HashSet<>(); - /** Starred server keys, read once and kept in step with the store. */ - private java.util.Set favouriteServerKeys = java.util.Collections.emptySet(); - - private void showProgressDialog(String message) { - // Showing a dialog on a finishing Activity throws WindowManager.BadTokenException, which - // took the screen down instead of reporting whatever it was about to report. The operation - // still runs; only the dialog is skipped. - if (isFinishing() || isDestroyed()) { - return; - } - if (progressDialog == null) { - progressDialog = new org.chimeramc.client.ui.dialogs.LoadingDialog(this); - } - progressDialog.show(); - progressDialog.setMessage(message); - } - - private void hideProgressDialog() { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityContentListBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - DynamicAnim.applyPressScaleRecursively(binding.getRoot()); - - contentType = getIntent().getIntExtra(EXTRA_CONTENT_TYPE, TYPE_WORLDS); - - // Guest mode removes the server list. This screen can be reached by a deep link - // or a restore as well as from Content Management, so the refusal lives here rather than - // only on the button that opens it; showing the category and then failing to load it would - // be worse than not opening it at all. - if (contentType == TYPE_SERVERS - && org.chimeramc.client.core.privacy.PrivacyModes.current(this).hidesServers()) { - finish(); - return; - } - contentManager = ContentManager.getInstance(this); - versionManager = VersionManager.get(this); - - String storageTypeStr = getIntent().getStringExtra(EXTRA_CURRENT_STORAGE_TYPE); - if (storageTypeStr != null) { - currentStorageType = parseStorageType(storageTypeStr); - } else { - SharedPreferences prefs = getSharedPreferences("content_management", MODE_PRIVATE); - String savedType = prefs.getString("storage_type", "INTERNAL"); - currentStorageType = parseStorageType(savedType); - } - - setupActivityResultLaunchers(); - setupUI(); - setupObservers(); - // ContentListActivity can be the first screen in the process (deep link, restore after a - // process death, or opened straight from another app), and when it is, nobody has pointed - // ContentManager at the game's directories yet. ServerManager then has a null - // minecraftPeDir, so every add/delete server reported failure while worlds and packs - // happened to work. Pointing it at this screen's own resolution is self-sufficient and - // idempotent, so it also repairs the case where MainActivity wired a different storage - // type than the one the user picked here. - updateStorageDirectories(); - loadContent(); - } - - /** - * Wires ContentManager to the directories for this screen's current storage type. - * - * The directories are derived from the same {@code getGameDataDirForType} resolution the - * worlds and packs already use, so servers cannot drift onto a different root. It is a no-op - * when no instance is selected. - */ - private void updateStorageDirectories() { - File gameDataDir = getGameDataDirForType(currentStorageType); - if (gameDataDir == null) { - return; - } - contentManager.setStorageDirectories( - new File(gameDataDir, "minecraftWorlds"), - new File(gameDataDir, "resource_packs"), - new File(gameDataDir, "behavior_packs"), - new File(gameDataDir, "skin_packs"), - new File(gameDataDir, "Screenshots"), - new File(gameDataDir, "minecraftpe")); - } - - private void setupActivityResultLaunchers() { - exportLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null && pendingExportWorld != null) { - Uri uri = result.getData().getData(); - if (uri != null) { - exportWorld(pendingExportWorld, uri); - } - } - pendingExportWorld = null; - } - ); - - exportPackLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null && pendingExportPack != null) { - Uri uri = result.getData().getData(); - if (uri != null) { - exportPack(pendingExportPack, uri); - } - } - pendingExportPack = null; - } - ); - - customFlatWorldLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - loadContent(); - } - } - ); - - structureExportLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null && pendingStructureExportWorld != null && pendingStructureInfo != null) { - Uri uri = result.getData().getData(); - if (uri != null) { - exportStructureToFile(pendingStructureExportWorld, pendingStructureInfo, uri); - } - } - pendingStructureExportWorld = null; - pendingStructureInfo = null; - } - ); - - structureExtractor = new StructureExtractor(this); - } - - private void setupUI() { - - String worldsPath = getIntent().getStringExtra(EXTRA_WORLDS_DIRECTORY); - if (worldsPath != null) { - worldsDirectory = new File(worldsPath); - } else { - worldsDirectory = getWorldsDirectoryForType(currentStorageType); - } - - switch (contentType) { - case TYPE_WORLDS: - binding.titleText.setText(getString(R.string.worlds_title)); - binding.customFlatButton.setVisibility(View.VISIBLE); - setupWorldsRecyclerView(); - break; - case TYPE_SKIN_PACKS: - binding.titleText.setText(getString(R.string.skin_packs_title)); - setupPacksRecyclerView(); - break; - case TYPE_RESOURCE_PACKS: - binding.titleText.setText(getString(R.string.resource_packs_title)); - setupPacksRecyclerView(); - break; - case TYPE_BEHAVIOR_PACKS: - binding.titleText.setText(getString(R.string.behavior_packs_title)); - setupPacksRecyclerView(); - break; - case TYPE_SCREENSHOTS: - binding.titleText.setText(getString(R.string.screenshots_category)); - binding.searchEditText.setVisibility(View.GONE); - setupScreenshotsRecyclerView(); - break; - case TYPE_SERVERS: - binding.titleText.setText(getString(R.string.servers_category)); - binding.searchEditText.setVisibility(View.VISIBLE); - binding.customFlatButton.setText(getString(R.string.quick_launch_add_server)); - binding.customFlatButton.setVisibility(View.VISIBLE); - // Read the stars before the adapter is built so the first paint is already - // favourites-first rather than reordering once the list arrives. - favouriteServerKeys = org.chimeramc.client.core.content.ServerFavouritesStore.load(this); - setupServersRecyclerView(); - break; - } - - binding.customFlatButton.setOnClickListener(v -> { - if (contentType == TYPE_SERVERS) { - showAddServerDialog(); - } else { - openCustomFlatWorld(); - } - }); - - setupSearchFilter(); - } - - private void setupSearchFilter() { - binding.searchEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterContent(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) {} - }); - } - - private void filterContent(String query) { - String lowerQuery = query.toLowerCase().trim(); - boolean empty = false; - if (contentType == TYPE_WORLDS) { - if (lowerQuery.isEmpty()) { - worldsAdapter.updateWorlds(allWorlds); - } else { - List filtered = allWorlds.stream() - .filter(world -> world.getWorldName().toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - worldsAdapter.updateWorlds(filtered); - } - empty = worldsAdapter.getItemCount() == 0; - setEmptyState(empty, R.string.content_empty_worlds_title, R.string.content_empty_worlds_message); - } else if (contentType == TYPE_SERVERS) { - if (lowerQuery.isEmpty()) { - serversAdapter.updateData(allServers); - } else { - List filtered = allServers.stream() - .filter(server -> server.name.toLowerCase().contains(lowerQuery) || - server.ip.toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - serversAdapter.updateData(filtered); - } - empty = serversAdapter.getItemCount() == 0; - setEmptyState(empty, R.string.content_empty_servers_title, R.string.content_empty_servers_message); - } else if (contentType == TYPE_SCREENSHOTS) { - empty = screenshotsAdapter.getItemCount() == 0; - setEmptyState(empty, R.string.content_empty_screenshots_title, R.string.content_empty_screenshots_message); - } else { - if (lowerQuery.isEmpty()) { - packsAdapter.updateResourcePacks(allPacks); - } else { - List filtered = allPacks.stream() - .filter(pack -> pack.getPackName().toLowerCase().contains(lowerQuery)) - .collect(Collectors.toList()); - packsAdapter.updateResourcePacks(filtered); - } - empty = packsAdapter.getItemCount() == 0; - setEmptyState(empty, R.string.content_empty_packs_title, R.string.content_empty_packs_message); - } - } - - private void setEmptyState(boolean empty, int titleRes, int messageRes) { - View emptyContainer = binding.contentEmpty; - if (emptyContainer == null) return; - emptyContainer.setVisibility(empty ? View.VISIBLE : View.GONE); - if (empty) { - binding.contentRecyclerView.setVisibility(View.GONE); - binding.contentEmptyTitle.setText(getString(titleRes)); - binding.contentEmptyMessage.setText(getString(messageRes)); - } else { - binding.contentRecyclerView.setVisibility(View.VISIBLE); - } - } - - private void setupWorldsRecyclerView() { - worldsAdapter = new WorldsAdapter(); - worldsAdapter.setOnWorldActionListener(new WorldsAdapter.OnWorldActionListener() { - @Override - public void onWorldExport(WorldItem world) { - startWorldExport(world); - } - - @Override - public void onWorldDelete(WorldItem world) { - showDeleteWorldDialog(world); - } - - @Override - public void onWorldBackup(WorldItem world) { - backupWorld(world); - } - - @Override - public void onWorldEdit(WorldItem world) { - openWorldEditor(world); - } - - @Override - public void onWorldExtractStructures(WorldItem world) { - showExtractStructuresDialog(world); - } - - @Override - public void onWorldTransfer(WorldItem world) { - showTransferWorldDialog(world); - } - }); - - binding.contentRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - binding.contentRecyclerView.setAdapter(worldsAdapter); - binding.contentRecyclerView.post(() -> DynamicAnim.staggerRecyclerChildren(binding.contentRecyclerView)); - } - - private void openWorldEditor(WorldItem world) { - File worldFile = world.getFile(); - if (worldFile == null || !worldFile.exists()) { - Toast.makeText(this, R.string.world_directory_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - Intent intent = new Intent(this, WorldEditorActivity.class); - intent.putExtra(WorldEditorActivity.EXTRA_WORLD_PATH, worldFile.getAbsolutePath()); - intent.putExtra(WorldEditorActivity.EXTRA_WORLD_NAME, world.getWorldName()); - startActivity(intent); - } - - private void setupPacksRecyclerView() { - packsAdapter = new ResourcePacksAdapter(); - packsAdapter.setOnResourcePackActionListener(new ResourcePacksAdapter.OnResourcePackActionListener() { - @Override - public void onResourcePackDelete(ResourcePackItem pack) { - showDeletePackDialog(pack); - } - - @Override - public void onResourcePackTransfer(ResourcePackItem pack) { - showTransferPackDialog(pack); - } - - @Override - public void onResourcePackExport(ResourcePackItem pack) { - startPackExport(pack); - } - }); - - binding.contentRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - binding.contentRecyclerView.setAdapter(packsAdapter); - binding.contentRecyclerView.post(() -> DynamicAnim.staggerRecyclerChildren(binding.contentRecyclerView)); - } - - private void setupScreenshotsRecyclerView() { - screenshotsAdapter = new org.chimeramc.client.ui.adapter.ScreenshotsAdapter(new ArrayList(), new org.chimeramc.client.ui.adapter.ScreenshotsAdapter.OnScreenshotClickListener() { - @Override - public void onDeleteClick(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - showDeleteScreenshotDialog(screenshot); - } - - @Override - public void onSaveClick(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - saveScreenshotToGallery(screenshot); - } - - @Override - public void onShareClick(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - org.chimeramc.client.launcher.core.content.ScreenshotShareSheet.shareItem( - ContentListActivity.this, screenshot); - } - }); - binding.contentRecyclerView.setLayoutManager(new androidx.recyclerview.widget.GridLayoutManager(this, 2)); - binding.contentRecyclerView.setAdapter(screenshotsAdapter); - binding.contentRecyclerView.post(() -> DynamicAnim.staggerRecyclerChildren(binding.contentRecyclerView)); - } - - private void setupServersRecyclerView() { - serversAdapter = new org.chimeramc.client.ui.adapter.ServersAdapter( - new ArrayList<>(), - new org.chimeramc.client.ui.adapter.ServersAdapter.OnServerClickListener() { - @Override - public void onDeleteClick(ServerItem server) { - showDeleteServerDialog(server); - } - - @Override - public void onFavouriteToggle(ServerItem server) { - // The star is local presentation state, so the write is a plain - // SharedPreferences apply and the list re-sorts immediately. - favouriteServerKeys = org.chimeramc.client.core.content.ServerFavouritesStore.toggle( - ContentListActivity.this, - org.chimeramc.client.core.content.ServerFavourites.key(server)); - serversAdapter.setFavourites(favouriteServerKeys); - } - }); - GameVersion currentVersion = versionManager.getSelectedVersion(); - serversAdapter.setSelectedVersionCode( - currentVersion == null ? null : currentVersion.versionCode); - serversAdapter.setFavourites(favouriteServerKeys); - binding.contentRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - binding.contentRecyclerView.setAdapter(serversAdapter); - binding.contentRecyclerView.post(() -> DynamicAnim.staggerRecyclerChildren(binding.contentRecyclerView)); - } - - /** - * Pings every listed server on a small background executor and posts the result back. The - * executor is created lazily so the servers tab pays nothing while it is not visible. - */ - private void startServerPings(List servers) { - if (servers == null || servers.isEmpty()) return; - List toPing = new ArrayList<>(); - for (org.chimeramc.client.core.content.ServerItem server : servers) { - String key = org.chimeramc.client.ui.adapter.ServersAdapter.keyFor(server); - if (pingedServerKeys.contains(key)) continue; - toPing.add(server); - pingedServerKeys.add(key); - } - if (toPing.isEmpty()) return; - synchronized (this) { - if (pingExecutor == null) { - pingExecutor = Executors.newFixedThreadPool(3); - } - } - ExecutorService executor = pingExecutor; - for (org.chimeramc.client.core.content.ServerItem server : toPing) { - // A ping for a row the user has just scrolled away from is submitted while the screen is - // still alive, but a refresh can race onDestroy and shut the pool down between the check - // and this submit. The row would then keep saying "checking" forever, so a rejected ping - // is reported as an offline result instead. - try { - executor.execute(() -> { - // The row renders "checking" until a result arrives, so a throw here left that - // server showing a ping check that could never finish. A failure is reported as - // an offline result rather than as no result. - ServerPongInfo pong; - try { - pong = RakNetPing.ping(server.ip, server.port, 3000); - } catch (Throwable t) { - Log.w(TAG, "Server ping failed for " + server.ip + ":" + server.port, t); - pong = null; - } - final ServerPongInfo result = pong; - boolean failed = result == null; - mainHandler.post(() -> { - if (serversAdapter != null) { - serversAdapter.onPingResult(server, result, failed); - } - }); - }); - } catch (java.util.concurrent.RejectedExecutionException rejected) { - mainHandler.post(() -> { - if (serversAdapter != null) { - serversAdapter.onPingResult(server, null, true); - } - }); - } - } - } - - private void setupObservers() { - switch (contentType) { - case TYPE_WORLDS: - contentManager.getWorldsLiveData().observe(this, worlds -> { - allWorlds = worlds != null ? worlds : new ArrayList<>(); - if (worldsAdapter != null) { - filterContent(binding.searchEditText.getText().toString()); - } - showLoading(false); - }); - break; - case TYPE_SKIN_PACKS: - contentManager.getSkinPacksLiveData().observe(this, packs -> { - allPacks = packs != null ? packs : new ArrayList<>(); - if (packsAdapter != null) { - filterContent(binding.searchEditText.getText().toString()); - } - showLoading(false); - }); - break; - case TYPE_RESOURCE_PACKS: - contentManager.getResourcePacksLiveData().observe(this, packs -> { - allPacks = packs != null ? packs : new ArrayList<>(); - if (packsAdapter != null) { - filterContent(binding.searchEditText.getText().toString()); - } - showLoading(false); - }); - break; - case TYPE_BEHAVIOR_PACKS: - contentManager.getBehaviorPacksLiveData().observe(this, packs -> { - allPacks = packs != null ? packs : new ArrayList<>(); - if (packsAdapter != null) { - filterContent(binding.searchEditText.getText().toString()); - } - showLoading(false); - }); - break; - case TYPE_SCREENSHOTS: - contentManager.getScreenshotsLiveData().observe(this, screenshots -> { - if (screenshotsAdapter != null) { - screenshotsAdapter.updateData(screenshots != null ? screenshots : new ArrayList<>()); - } - showLoading(false); - }); - break; - case TYPE_SERVERS: - contentManager.getServersLiveData().observe(this, servers -> { - allServers = servers != null ? servers : new ArrayList<>(); - if (serversAdapter != null) { - filterContent(binding.searchEditText.getText().toString()); - startServerPings(allServers); - } - showLoading(false); - }); - break; - } - } - - private void loadContent() { - showLoading(true); - switch (contentType) { - case TYPE_WORLDS: - contentManager.refreshWorlds(); - break; - case TYPE_SKIN_PACKS: - contentManager.refreshSkinPacks(); - break; - case TYPE_RESOURCE_PACKS: - contentManager.refreshResourcePacks(); - break; - case TYPE_BEHAVIOR_PACKS: - contentManager.refreshBehaviorPacks(); - break; - case TYPE_SCREENSHOTS: - contentManager.refreshScreenshots(); - break; - case TYPE_SERVERS: - contentManager.refreshServers(); - break; - } - } - - private void showLoading(boolean show) { - binding.loadingOverlay.setVisibility(show ? View.VISIBLE : View.GONE); - } - - private void startWorldExport(WorldItem world) { - pendingExportWorld = world; - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/zip"); - intent.putExtra(Intent.EXTRA_TITLE, world.getName() + ".mcworld"); - exportLauncher.launch(intent); - } - - private void exportWorld(WorldItem world, Uri uri) { - showProgressDialog(getString(R.string.exporting_world)); - contentManager.exportWorld(world, uri, new WorldManager.WorldOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void startPackExport(ResourcePackItem pack) { - pendingExportPack = pack; - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/zip"); - intent.putExtra(Intent.EXTRA_TITLE, pack.getPackName() + ".mcpack"); - exportPackLauncher.launch(intent); - } - - private void exportPack(ResourcePackItem pack, Uri uri) { - showProgressDialog(getString(R.string.exporting_pack)); - contentManager.exportResourcePack(pack, uri, new ResourcePackManager.PackOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void backupWorld(WorldItem world) { - showProgressDialog(getString(R.string.backing_up_world)); - contentManager.backupWorld(world, new WorldManager.WorldOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void showDeleteWorldDialog(WorldItem world) { - new CustomAlertDialog(this) - .setTitleText(getString(R.string.delete_world)) - .setMessage(getString(R.string.confirm_delete_world)) - .setPositiveButton(getString(R.string.dialog_positive_delete), v -> deleteWorld(world)) - .setNegativeButton(getString(R.string.cancel), null) - .show(); - } - - private void deleteWorld(WorldItem world) { - showProgressDialog(getString(R.string.deleting_world)); - contentManager.deleteWorld(world, new WorldManager.WorldOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void showDeletePackDialog(ResourcePackItem pack) { - int titleResId; - int messageResId; - - if (contentType == TYPE_BEHAVIOR_PACKS) { - titleResId = R.string.delete_behavior_pack; - messageResId = R.string.confirm_delete_behavior_pack; - } else if (contentType == TYPE_SKIN_PACKS) { - titleResId = R.string.delete_skin_pack; - messageResId = R.string.confirm_delete_skin_pack; - } else { - titleResId = R.string.delete_resource_pack; - messageResId = R.string.confirm_delete_resource_pack; - } - - new CustomAlertDialog(this) - .setTitleText(getString(titleResId)) - .setMessage(getString(messageResId)) - .setPositiveButton(getString(R.string.dialog_positive_delete), v -> deletePack(pack)) - .setNegativeButton(getString(R.string.cancel), null) - .show(); - } - - private void deletePack(ResourcePackItem pack) { - showProgressDialog(getString(R.string.deleting_pack)); - contentManager.deleteResourcePack(pack, new ResourcePackManager.PackOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void saveScreenshotToGallery(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - showLoading(true); - AppExecutors.run(AppExecutors.Kind.DISK, "screenshot-export", () -> { - try { - Bitmap bitmap = BitmapFactory.decodeFile(screenshot.file.getAbsolutePath()); - if (bitmap != null) { - ContentValues values = new ContentValues(); - values.put(MediaStore.Images.Media.DISPLAY_NAME, screenshot.name + "_" + System.currentTimeMillis() + ".jpg"); - values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); - values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000); - - Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); - if (uri != null) { - try (OutputStream out = getContentResolver().openOutputStream(uri)) { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); - } - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, R.string.saved_to_gallery, Toast.LENGTH_SHORT).show(); - }); - return; - } - } - } catch (Exception e) { - AppLog.warn(TAG, "Opening the content link failed", e); - } - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, R.string.save_failed, Toast.LENGTH_SHORT).show(); - }); - }); - } - - private void showDeleteScreenshotDialog(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - new CustomAlertDialog(this) - .setTitleText(getString(R.string.delete)) - .setMessage(getString(R.string.delete_screenshot_confirm)) - .setPositiveButton(getString(R.string.dialog_positive_delete), v -> deleteScreenshot(screenshot)) - .setNegativeButton(getString(R.string.cancel), null) - .show(); - } - - private void showAddServerDialog() { - View dialogView = getLayoutInflater().inflate(R.layout.dialog_add_server, null); - EditText serverNameEdit = dialogView.findViewById(R.id.server_name_edit); - EditText serverIpEdit = dialogView.findViewById(R.id.server_ip_edit); - EditText serverPortEdit = dialogView.findViewById(R.id.server_port_edit); - serverPortEdit.setText("19132"); - - new CustomAlertDialog(this) - .setTitleText(getString(R.string.quick_launch_add_server)) - .setCustomView(dialogView) - .setPositiveButton(getString(R.string.add), v -> { - String name = serverNameEdit.getText().toString().trim(); - String ip = serverIpEdit.getText().toString().trim(); - String portStr = serverPortEdit.getText().toString().trim(); - - if (TextUtils.isEmpty(name) || TextUtils.isEmpty(ip)) { - Toast.makeText(this, R.string.server_details_required, Toast.LENGTH_SHORT).show(); - return; - } - - int port = 19132; - if (!TextUtils.isEmpty(portStr)) { - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - Toast.makeText(this, R.string.invalid_port, Toast.LENGTH_SHORT).show(); - return; - } - } - - addServer(new org.chimeramc.client.core.content.ServerItem(name, ip, port)); - }) - .setNegativeButton(getString(R.string.cancel), null) - .show(); - } - - private void addServer(org.chimeramc.client.core.content.ServerItem server) { - showProgressDialog(getString(R.string.adding_server)); - contentManager.addServer(server, new ContentManager.ContentOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void deleteScreenshot(org.chimeramc.client.core.content.ScreenshotItem screenshot) { - showProgressDialog(getString(R.string.deleting_screenshot)); - contentManager.deleteScreenshot(screenshot, new ContentManager.ContentOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void showDeleteServerDialog(org.chimeramc.client.core.content.ServerItem server) { - new CustomAlertDialog(this) - .setTitleText(getString(R.string.delete)) - .setMessage(getString(R.string.delete_server_confirm)) - .setPositiveButton(getString(R.string.dialog_positive_delete), v -> deleteServer(server)) - .setNegativeButton(getString(R.string.cancel), null) - .show(); - } - - private void deleteServer(org.chimeramc.client.core.content.ServerItem server) { - showProgressDialog(getString(R.string.deleting_server)); - contentManager.deleteServer(server, new ContentManager.ContentOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, message, Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void openCustomFlatWorld() { - if (worldsDirectory == null || !worldsDirectory.exists()) { - Toast.makeText(this, R.string.worlds_directory_unavailable, Toast.LENGTH_SHORT).show(); - return; - } - - Intent intent = new Intent(this, CustomFlatWorldActivity.class); - intent.putExtra(CustomFlatWorldActivity.EXTRA_WORLDS_DIRECTORY, worldsDirectory.getAbsolutePath()); - customFlatWorldLauncher.launch(intent); - } - - private void showExtractStructuresDialog(WorldItem world) { - File worldFile = world.getFile(); - if (worldFile == null || !worldFile.exists()) { - Toast.makeText(this, R.string.world_directory_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - binding.loadingOverlay.setVisibility(View.VISIBLE); - - structureExtractor.loadStructures(worldFile, new StructureExtractor.StructureListCallback() { - @Override - public void onComplete(List structures) { - runOnUiThread(() -> { - binding.loadingOverlay.setVisibility(View.GONE); - if (structures.isEmpty()) { - showNoStructuresFoundDialog(); - } else { - showStructureSelectionDialog(world, structures); - } - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - binding.loadingOverlay.setVisibility(View.GONE); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void showNoStructuresFoundDialog() { - new CustomAlertDialog(this) - .setTitleText(getString(R.string.no_structures_found_title)) - .setMessage(getString(R.string.no_structures_found_message)) - .setPositiveButton(getString(R.string.dialog_positive_ok), null) - .show(); - } - - private void showStructureSelectionDialog(WorldItem world, List structures) { - View dialogView = getLayoutInflater().inflate(R.layout.dialog_structure_list, null); - - TextView structureCount = dialogView.findViewById(R.id.structure_count); - RecyclerView recyclerView = dialogView.findViewById(R.id.structures_recycler_view); - - structureCount.setText(getResources().getQuantityString(R.plurals.structures_found_count, structures.size(), structures.size())); - - StructuresAdapter adapter = new StructuresAdapter(); - adapter.setStructures(structures); - - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setAdapter(adapter); - - AlertDialog dialog = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.structures_found_title) - .setView(dialogView) - .setNegativeButton(R.string.cancel, null) - .create(); - - adapter.setOnStructureExportListener(structure -> { - dialog.dismiss(); - startStructureExport(world, structure); - }); - - dialog.show(); - - org.chimeramc.client.util.PersonalizationManager structPm = new org.chimeramc.client.util.PersonalizationManager(this); - int structAccent = structPm.getAccentColor(); - if (structAccent != 0) { - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(structAccent); - } - } - - private void startStructureExport(WorldItem world, StructureExtractor.StructureInfo structure) { - pendingStructureExportWorld = world; - pendingStructureInfo = structure; - - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("application/octet-stream"); - intent.putExtra(Intent.EXTRA_TITLE, structure.getFileName()); - structureExportLauncher.launch(intent); - } - - private void exportStructureToFile(WorldItem world, StructureExtractor.StructureInfo structure, Uri uri) { - binding.loadingOverlay.setVisibility(View.VISIBLE); - - structureExtractor.exportSingleStructure(structure, uri, new StructureExtractor.ExtractionCallback() { - - @Override - public void onComplete(int extractedCount, String outputPath) { - runOnUiThread(() -> { - binding.loadingOverlay.setVisibility(View.GONE); - Toast.makeText(ContentListActivity.this, - getString(R.string.structure_exported, structure.getName()), - Toast.LENGTH_SHORT).show(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - binding.loadingOverlay.setVisibility(View.GONE); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - }); - } - - private void showTransferWorldDialog(WorldItem world) { - View dialogView = getLayoutInflater().inflate(R.layout.dialog_transfer_content, null); - RadioGroup radioGroup = dialogView.findViewById(R.id.storage_radio_group); - RadioButton radioInternal = dialogView.findViewById(R.id.radio_internal); - RadioButton radioExternal = dialogView.findViewById(R.id.radio_external); - RadioButton radioVersionIsolationInternal = dialogView.findViewById(R.id.radio_version_isolation_internal); - RadioButton radioVersionIsolation = dialogView.findViewById(R.id.radio_version_isolation); - radioVersionIsolationInternal.setText(getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_internal) + ")"); - radioVersionIsolation.setText(getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_external) + ")"); - - switch (currentStorageType) { - case INTERNAL -> radioInternal.setEnabled(false); - case EXTERNAL -> radioExternal.setEnabled(false); - case VERSION_ISOLATION_INTERNAL -> radioVersionIsolationInternal.setEnabled(false); - case VERSION_ISOLATION, VERSION_ISOLATION_EXTERNAL -> radioVersionIsolation.setEnabled(false); - } - - AlertDialog dialog = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.transfer_content) - .setView(dialogView) - .setPositiveButton(R.string.transfer, (d, which) -> { - int selectedId = radioGroup.getCheckedRadioButtonId(); - FeatureSettings.StorageType targetType = null; - - if (selectedId == R.id.radio_internal) { - targetType = FeatureSettings.StorageType.INTERNAL; - } else if (selectedId == R.id.radio_external) { - targetType = FeatureSettings.StorageType.EXTERNAL; - } else if (selectedId == R.id.radio_version_isolation_internal) { - targetType = FeatureSettings.StorageType.VERSION_ISOLATION_INTERNAL; - } else if (selectedId == R.id.radio_version_isolation) { - targetType = FeatureSettings.StorageType.VERSION_ISOLATION_EXTERNAL; - } - - if (targetType != null && targetType != currentStorageType) { - transferWorld(world, targetType); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); - - org.chimeramc.client.util.PersonalizationManager twPm = new org.chimeramc.client.util.PersonalizationManager(this); - int twAccent = twPm.getAccentColor(); - if (twAccent != 0) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(twAccent); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(twAccent); - } else { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getResources().getColor(R.color.accent_text, getTheme())); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(getResources().getColor(R.color.accent_text, getTheme())); - } - } - - private void showTransferPackDialog(ResourcePackItem pack) { - View dialogView = getLayoutInflater().inflate(R.layout.dialog_transfer_content, null); - RadioGroup radioGroup = dialogView.findViewById(R.id.storage_radio_group); - RadioButton radioInternal = dialogView.findViewById(R.id.radio_internal); - RadioButton radioExternal = dialogView.findViewById(R.id.radio_external); - RadioButton radioVersionIsolationInternal = dialogView.findViewById(R.id.radio_version_isolation_internal); - RadioButton radioVersionIsolation = dialogView.findViewById(R.id.radio_version_isolation); - radioVersionIsolationInternal.setText(getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_internal) + ")"); - radioVersionIsolation.setText(getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_external) + ")"); - - switch (currentStorageType) { - case INTERNAL -> radioInternal.setEnabled(false); - case EXTERNAL -> radioExternal.setEnabled(false); - case VERSION_ISOLATION_INTERNAL -> radioVersionIsolationInternal.setEnabled(false); - case VERSION_ISOLATION, VERSION_ISOLATION_EXTERNAL -> radioVersionIsolation.setEnabled(false); - } - - AlertDialog dialog = new MaterialAlertDialogBuilder(this) - .setTitle(R.string.transfer_content) - .setView(dialogView) - .setPositiveButton(R.string.transfer, (d, which) -> { - int selectedId = radioGroup.getCheckedRadioButtonId(); - FeatureSettings.StorageType targetType = null; - - if (selectedId == R.id.radio_internal) { - targetType = FeatureSettings.StorageType.INTERNAL; - } else if (selectedId == R.id.radio_external) { - targetType = FeatureSettings.StorageType.EXTERNAL; - } else if (selectedId == R.id.radio_version_isolation_internal) { - targetType = FeatureSettings.StorageType.VERSION_ISOLATION_INTERNAL; - } else if (selectedId == R.id.radio_version_isolation) { - targetType = FeatureSettings.StorageType.VERSION_ISOLATION_EXTERNAL; - } - - if (targetType != null && targetType != currentStorageType) { - transferPack(pack, targetType); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); - - org.chimeramc.client.util.PersonalizationManager tpPm = new org.chimeramc.client.util.PersonalizationManager(this); - int tpAccent = tpPm.getAccentColor(); - if (tpAccent != 0) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(tpAccent); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(tpAccent); - } else { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getResources().getColor(R.color.accent_text, getTheme())); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(getResources().getColor(R.color.accent_text, getTheme())); - } - } - - private void transferWorld(WorldItem world, FeatureSettings.StorageType targetType) { - File targetDir = getWorldsDirectoryForType(targetType); - if (targetDir == null) { - Toast.makeText(this, getString(R.string.transfer_failed), Toast.LENGTH_SHORT).show(); - return; - } - - showLoading(true); - contentManager.transferWorld(world, targetDir, new WorldManager.WorldOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, getString(R.string.transfer_success), Toast.LENGTH_SHORT).show(); - loadContent(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private void transferPack(ResourcePackItem pack, FeatureSettings.StorageType targetType) { - File targetDir = getPackDirectoryForType(targetType, pack.isBehaviorPack() ? "behavior_packs" : - (contentType == TYPE_SKIN_PACKS ? "skin_packs" : "resource_packs")); - if (targetDir == null) { - Toast.makeText(this, getString(R.string.transfer_failed), Toast.LENGTH_SHORT).show(); - return; - } - - showLoading(true); - contentManager.transferResourcePack(pack, targetDir, new ResourcePackManager.PackOperationCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, getString(R.string.transfer_success), Toast.LENGTH_SHORT).show(); - loadContent(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - showLoading(false); - Toast.makeText(ContentListActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) {} - }); - } - - private File getWorldsDirectoryForType(FeatureSettings.StorageType storageType) { - File gameDataDir = getGameDataDirForType(storageType); - return gameDataDir == null ? null : new File(gameDataDir, "minecraftWorlds"); - } - - private File getPackDirectoryForType(FeatureSettings.StorageType storageType, String packType) { - File gameDataDir = getGameDataDirForType(storageType); - return gameDataDir == null ? null : new File(gameDataDir, packType); - } - - private File getGameDataDirForType(FeatureSettings.StorageType storageType) { - GameVersion currentVersion = versionManager.getSelectedVersion(); - // With no instance selected there is still a place the game keeps its servers, and that is - // the shared root. Returning null here meant ServerManager was handed a null directory and - // every "add server" reported failure until an instance happened to be selected — the - // "servers don't work" report. The shared resolution needs no profile, so it is a sound - // fallback rather than a guess. - if (currentVersion == null) { - return LauncherStorage.getContentGameDataDir( - this, - LauncherStorage.INSTALLED_MINECRAFT_PROFILE_ID, - LauncherStorage.normalizeContentStorageType(storageType, false) - ); - } - FeatureSettings.StorageType resolvedType = LauncherStorage.normalizeContentStorageType( - storageType, - currentVersion.versionIsolation - ); - return LauncherStorage.getContentGameDataDir(this, currentVersion.getStorageProfileId(), resolvedType); - } - - private FeatureSettings.StorageType parseStorageType(String value) { - try { - return FeatureSettings.StorageType.valueOf(value); - } catch (Exception ignored) { - return FeatureSettings.StorageType.INTERNAL; - } - } - - @Override - protected void onResume() { - super.onResume(); - loadContent(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (structureExtractor != null) { - structureExtractor.shutdown(); - } - if (pingExecutor != null) { - pingExecutor.shutdownNow(); - pingExecutor = null; - } - // The screenshots adapter owns a decode pool; without this its four worker threads outlive - // the screen and stay parked for the life of the process. - if (screenshotsAdapter != null) { - screenshotsAdapter.shutdown(); - screenshotsAdapter = null; - } - // The pack adapter owns a background size/resolution scanner; it must be - // stopped with the screen or its worker outlives the activity. - if (packsAdapter != null) { - packsAdapter.dispose(); - packsAdapter = null; - } - // A dialog still attached to a destroyed Activity is the "Activity has leaked window" - // crash; it also trapped the user, because nothing could dismiss it once the owner was gone. - hideProgressDialog(); - } -} diff --git a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentManagementActivity.java b/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentManagementActivity.java deleted file mode 100644 index f3929a69..00000000 --- a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/ContentManagementActivity.java +++ /dev/null @@ -1,368 +0,0 @@ -package org.chimeramc.client.ui.activities; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; - -import org.chimeramc.client.R; -import org.chimeramc.client.core.content.ContentImporter; -import org.chimeramc.client.core.content.ContentManager; -import org.chimeramc.client.core.versions.GameVersion; -import org.chimeramc.client.core.versions.VersionManager; -import org.chimeramc.client.databinding.ActivityContentManagementBinding; -import org.chimeramc.client.settings.FeatureSettings; -import org.chimeramc.client.ui.animation.DynamicAnim; -import org.chimeramc.client.util.LauncherStorage; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -public class ContentManagementActivity extends BaseActivity { - - private static final String PREFS_NAME = "content_management"; - private static final String KEY_STORAGE_TYPE = "storage_type"; - - private ActivityContentManagementBinding binding; - private ContentManager contentManager; - private ContentImporter contentImporter; - private VersionManager versionManager; - private FeatureSettings.StorageType currentStorageType = FeatureSettings.StorageType.INTERNAL; - private SharedPreferences prefs; - private ActivityResultLauncher importLauncher; - private org.chimeramc.client.ui.dialogs.LoadingDialog progressDialog; - - private void showProgressDialog(String message) { - // Showing a dialog on a finishing Activity throws WindowManager.BadTokenException, which - // took the screen down instead of reporting whatever it was about to report. The operation - // still runs; only the dialog is skipped. - if (isFinishing() || isDestroyed()) { - return; - } - if (progressDialog == null) { - progressDialog = new org.chimeramc.client.ui.dialogs.LoadingDialog(this); - } - progressDialog.show(); - progressDialog.setMessage(message); - } - - private void hideProgressDialog() { - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityContentManagementBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - DynamicAnim.applyPressScaleRecursively(binding.getRoot()); - - setupActivityResultLaunchers(); - initializeManagers(); - setupUI(); - loadCurrentVersion(); - } - - private void setupActivityResultLaunchers() { - importLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null) { - Intent data = result.getData(); - List uris = new ArrayList<>(); - if (data.getClipData() != null) { - int count = data.getClipData().getItemCount(); - for (int i = 0; i < count; i++) { - uris.add(data.getClipData().getItemAt(i).getUri()); - } - } else if (data.getData() != null) { - uris.add(data.getData()); - } - if (!uris.isEmpty()) { - handleImport(uris); - } - } - } - ); - } - - private void initializeManagers() { - contentManager = ContentManager.getInstance(this); - contentImporter = new ContentImporter(this); - versionManager = VersionManager.get(this); - prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - loadStorageType(); - } - - private void loadStorageType() { - String savedType = prefs.getString(KEY_STORAGE_TYPE, "INTERNAL"); - currentStorageType = parseStorageType(savedType); - normalizeCurrentStorageType(); - } - - private void saveStorageType() { - prefs.edit().putString(KEY_STORAGE_TYPE, currentStorageType.name()).apply(); - } - - private void setupUI() { - binding.editOptionsButton.setOnClickListener(v -> openOptionsEditor()); - binding.importContentButton.setOnClickListener(v -> startImport()); - - setupStorageSpinner(); - setupCategoryButtons(); - } - - private void startImport() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"application/zip", "application/octet-stream"}); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - importLauncher.launch(Intent.createChooser(intent, getString(R.string.import_content_title))); - } - - private void handleImport(List uris) { - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion == null) { - Toast.makeText(this, R.string.not_found_version, Toast.LENGTH_SHORT).show(); - return; - } - - File worldsDir = getWorldsDirectory(); - File resourcePacksDir = getPackDirectory("resource_packs"); - File behaviorPacksDir = getPackDirectory("behavior_packs"); - File skinPacksDir = getPackDirectory("skin_packs"); - - showProgressDialog(getString(R.string.importing_content)); - - contentImporter.importContent(uris, resourcePacksDir, behaviorPacksDir, skinPacksDir, worldsDir, - new ContentImporter.ImportCallback() { - @Override - public void onSuccess(String message) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentManagementActivity.this, message, Toast.LENGTH_SHORT).show(); - contentManager.refreshContent(); - }); - } - - @Override - public void onError(String error) { - runOnUiThread(() -> { - hideProgressDialog(); - Toast.makeText(ContentManagementActivity.this, error, Toast.LENGTH_LONG).show(); - }); - } - - @Override - public void onProgress(int progress) { - } - }); - } - - private File getPackDirectory(String packType) { - File gameDataDir = getGameDataDirForType(currentStorageType); - return gameDataDir == null ? null : new File(gameDataDir, packType); - } - - private void setupStorageSpinner() { - String[] storageOptions = { - getString(R.string.storage_internal), - getString(R.string.storage_external), - getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_internal) + ")", - getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_external) + ")" - }; - - ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.spinner_item, storageOptions); - adapter.setDropDownViewResource(R.layout.spinner_dropdown_item); - binding.storageTypeSpinner.setAdapter(adapter); - binding.storageTypeSpinner.setPopupBackgroundResource(R.drawable.bg_popup_menu_rounded); - DynamicAnim.applyPressScale(binding.storageTypeSpinner); - - int currentSelection = switch (currentStorageType) { - case INTERNAL -> 0; - case EXTERNAL -> 1; - case VERSION_ISOLATION_INTERNAL -> 2; - case VERSION_ISOLATION, VERSION_ISOLATION_EXTERNAL -> 3; - }; - binding.storageTypeSpinner.setSelection(currentSelection); - - binding.storageTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - FeatureSettings.StorageType newType = switch (position) { - case 0 -> FeatureSettings.StorageType.INTERNAL; - case 1 -> FeatureSettings.StorageType.EXTERNAL; - case 2 -> FeatureSettings.StorageType.VERSION_ISOLATION_INTERNAL; - case 3 -> FeatureSettings.StorageType.VERSION_ISOLATION_EXTERNAL; - default -> FeatureSettings.StorageType.VERSION_ISOLATION_EXTERNAL; - }; - - if (newType != currentStorageType) { - currentStorageType = newType; - saveStorageType(); - updateStorageDirectories(); - } - } - - @Override - public void onNothingSelected(AdapterView parent) {} - }); - } - - private void setupCategoryButtons() { - binding.worldsButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_WORLDS)); - binding.skinPacksButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_SKIN_PACKS)); - binding.resourcePacksButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_RESOURCE_PACKS)); - binding.behaviorPacksButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_BEHAVIOR_PACKS)); - binding.screenshotsButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_SCREENSHOTS)); - binding.serversButton.setOnClickListener(v -> openContentList(ContentListActivity.TYPE_SERVERS)); - // Guest mode removes the server list: a borrowed device should not show which - // servers the owner plays on, and this is the only entry to that category. - if (org.chimeramc.client.core.privacy.PrivacyModes.current(this).hidesServers()) { - binding.serversButton.setVisibility(View.GONE); - } - } - - private void openContentList(int contentType) { - Intent intent = new Intent(this, ContentListActivity.class); - intent.putExtra(ContentListActivity.EXTRA_CONTENT_TYPE, contentType); - intent.putExtra(ContentListActivity.EXTRA_CURRENT_STORAGE_TYPE, currentStorageType.name()); - - if (contentType == ContentListActivity.TYPE_WORLDS) { - File worldsDir = getWorldsDirectory(); - if (worldsDir != null) { - intent.putExtra(ContentListActivity.EXTRA_WORLDS_DIRECTORY, worldsDir.getAbsolutePath()); - } - } - - startActivity(intent); - } - - private File getWorldsDirectory() { - File gameDataDir = getGameDataDirForType(currentStorageType); - return gameDataDir == null ? null : new File(gameDataDir, "minecraftWorlds"); - } - - private void loadCurrentVersion() { - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion != null) { - binding.versionText.setText(currentVersion.displayName); - updateStorageDirectories(); - } else { - binding.versionText.setText(getString(R.string.not_found_version)); - Toast.makeText(this, R.string.dialog_title_no_version, Toast.LENGTH_SHORT).show(); - } - } - - private void updateStorageDirectories() { - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion == null) return; - - File worldsDir; - File resourcePacksDir; - File behaviorPacksDir; - File skinPacksDir; - File screenshotsDir; - File minecraftPeDir; - - File gameDataDir = getGameDataDirForType(currentStorageType); - if (gameDataDir == null) { - worldsDir = null; - resourcePacksDir = null; - behaviorPacksDir = null; - skinPacksDir = null; - screenshotsDir = null; - minecraftPeDir = null; - } else { - worldsDir = new File(gameDataDir, "minecraftWorlds"); - resourcePacksDir = new File(gameDataDir, "resource_packs"); - behaviorPacksDir = new File(gameDataDir, "behavior_packs"); - skinPacksDir = new File(gameDataDir, "skin_packs"); - screenshotsDir = new File(gameDataDir, "Screenshots"); - minecraftPeDir = new File(gameDataDir, "minecraftpe"); - } - - contentManager.setStorageDirectories(worldsDir, resourcePacksDir, behaviorPacksDir, skinPacksDir, screenshotsDir, minecraftPeDir); - } - - private void openOptionsEditor() { - File optionsFile = getOptionsFile(); - if (optionsFile == null) { - Toast.makeText(this, R.string.options_file_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - String storageLabel = switch (currentStorageType) { - case INTERNAL -> getString(R.string.storage_internal); - case EXTERNAL -> getString(R.string.storage_external); - case VERSION_ISOLATION_INTERNAL -> getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_internal) + ")"; - case VERSION_ISOLATION, VERSION_ISOLATION_EXTERNAL -> getString(R.string.storage_version_isolation) + " (" + getString(R.string.storage_external) + ")"; - }; - - Intent intent = new Intent(this, OptionsEditorActivity.class); - intent.putExtra(OptionsEditorActivity.EXTRA_OPTIONS_PATH, optionsFile.getAbsolutePath()); - intent.putExtra(OptionsEditorActivity.EXTRA_STORAGE_TYPE, storageLabel); - startActivity(intent); - } - - private File getOptionsFile() { - GameVersion currentVersion = versionManager.getSelectedVersion(); - - File gameDataDir = getGameDataDirForType(currentStorageType); - return gameDataDir == null ? null : new File(gameDataDir, "minecraftpe/options.txt"); - } - - private File getGameDataDirForType(FeatureSettings.StorageType storageType) { - GameVersion currentVersion = versionManager.getSelectedVersion(); - if (currentVersion == null) return null; - FeatureSettings.StorageType resolvedType = LauncherStorage.normalizeContentStorageType( - storageType, - currentVersion.versionIsolation - ); - return LauncherStorage.getContentGameDataDir(this, currentVersion.getStorageProfileId(), resolvedType); - } - - private void normalizeCurrentStorageType() { - GameVersion currentVersion = versionManager != null ? versionManager.getSelectedVersion() : null; - if (currentVersion == null) return; - currentStorageType = LauncherStorage.normalizeContentStorageType( - currentStorageType, - currentVersion.versionIsolation - ); - } - - private FeatureSettings.StorageType parseStorageType(String value) { - try { - return FeatureSettings.StorageType.valueOf(value); - } catch (Exception ignored) { - return FeatureSettings.StorageType.INTERNAL; - } - } - - @Override - protected void onResume() { - super.onResume(); - updateStorageDirectories(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (contentImporter != null) { - contentImporter.shutdown(); - } - // A dialog still attached to a destroyed Activity is the "Activity has leaked window" - // crash, and it trapped the user because nothing could dismiss it once the owner was gone. - hideProgressDialog(); - } -} diff --git a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.java b/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.java new file mode 100644 index 00000000..fcee8894 --- /dev/null +++ b/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.java @@ -0,0 +1,20 @@ +package org.chimeramc.client.ui.activities; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +/** + * Placeholder screen. + * + *

The legacy UI was removed in one sweep so a new one can be dropped in. This class survives + * only because non-UI code (the manifest, an Intent target, or a nav destination) still names it; + * it draws nothing. Replace it with the real CrashActivity screen. + */ +public class CrashActivity extends BaseActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} diff --git a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.kt b/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.kt deleted file mode 100644 index a16f8f0c..00000000 --- a/app/src/main/java/org/chimeramc/client/launcher/ui/activities/CrashActivity.kt +++ /dev/null @@ -1,320 +0,0 @@ -package org.chimeramc.client.ui.activities - -import org.chimeramc.client.util.AppLog - -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.widget.Button -import android.widget.TextView -import android.widget.Toast -import androidx.core.content.FileProvider -import org.chimeramc.client.R -import org.chimeramc.client.core.crash.CrashReporter -import org.chimeramc.client.core.crash.ProcessExitReasons -import org.chimeramc.client.core.mods.ModManager -import org.chimeramc.client.core.mods.ModSafeMode -import org.chimeramc.client.core.versions.GameVersion -import org.chimeramc.client.core.versions.VersionManager -import org.chimeramc.client.launcher.core.minecraft.LaunchErrorDiagnosis -import org.chimeramc.client.ui.animation.DynamicAnim -import java.io.File -import java.util.concurrent.Executors - -class CrashActivity : BaseActivity() { - - private var executor = Executors.newSingleThreadExecutor() - private var logPath: String? = null - private var disableButton: Button? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_crash) - - logPath = intent.getStringExtra("LOG_PATH") - val summary = intent.getStringExtra("SUMMARY") - ?: intent.getStringExtra("EMERGENCY") - val crashType = intent.getStringExtra("CRASH_TYPE") - // A crash screen means the previous process died before a session was recorded, so - // count it here rather than from the dying process -- an async write from a crashing process - // is not guaranteed to land. This screen runs in the fresh launcher process, where the - // write is durable. - org.chimeramc.client.core.stats.LaunchStatsRecorder.recordCrash(this) - CrashReporter.sendUnsentReports(this) - - val tvTitle = findViewById(R.id.crash_title) - val tvDetails = findViewById(R.id.crash_details) - val btnBack = findViewById