From a5efce53abb6fe773c01f533979db110593d8ee9 Mon Sep 17 00:00:00 2001 From: Shallow Date: Mon, 22 Jun 2026 10:57:37 +0800 Subject: [PATCH 01/10] Fix module repository loading Use the working backup repository API endpoint for module JSON, surface failed HTTP responses, and load full module details when README content is missing. Keeps the browser action on the public modules.lsposed.org web page because the backup host only serves the JSON API. Builds on the approach from #747 by byemaxx. Co-authored-by: Qing <44231502+byemaxx@users.noreply.github.com> --- .../org/lsposed/manager/repo/RepoLoader.java | 107 +++++++++--------- .../manager/ui/fragment/RepoItemFragment.java | 97 +++++++++++++++- 2 files changed, 148 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index bb4db3271..47798eed1 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -76,11 +76,7 @@ public boolean upgradable(long versionCode, String versionName) { private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json"); private final Set listeners = ConcurrentHashMap.newKeySet(); private boolean repoLoaded = false; - private static final String originRepoUrl = "https://modules.lsposed.org/"; - private static final String backupRepoUrl = "https://modules-blogcdn.lsposed.org/"; - - private static final String secondBackupRepoUrl = "https://modules-cloudflare.lsposed.org/"; - private static String repoUrl = originRepoUrl; + private static final String repoUrl = "https://backup.modules.lsposed.org/"; private final Resources resources = App.getInstance().getResources(); private final String[] channels = resources.getStringArray(R.array.update_channel_values); @@ -98,22 +94,25 @@ public static synchronized RepoLoader getInstance() { synchronized public void loadRemoteData() { repoLoaded = false; + boolean loaded = false; try { try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(repoUrl + "modules.json").build()).execute()) { - - if (response.isSuccessful()) { - ResponseBody body = response.body(); - if (body != null) { - try { - String bodyString = body.string(); - Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); - loadLocalData(false); - } catch (Throwable t) { - Log.e(App.TAG, Log.getStackTraceString(t)); - for (RepoListener listener : listeners) { - listener.onThrowable(t); - } - } + if (!response.isSuccessful()) { + throw new IOException("Unexpected response " + response.code() + " from " + response.request().url()); + } + ResponseBody body = response.body(); + if (body == null) { + throw new IOException("Empty response from " + response.request().url()); + } + try { + String bodyString = body.string(); + Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); + loadLocalData(false); + loaded = true; + } catch (Throwable t) { + Log.e(App.TAG, Log.getStackTraceString(t)); + for (RepoListener listener : listeners) { + listener.onThrowable(t); } } } @@ -122,12 +121,12 @@ synchronized public void loadRemoteData() { for (RepoListener listener : listeners) { listener.onThrowable(e); } - if (repoUrl.equals(originRepoUrl)) { - repoUrl = backupRepoUrl; - loadRemoteData(); - } else if (repoUrl.equals(backupRepoUrl)) { - repoUrl = secondBackupRepoUrl; - loadRemoteData(); + } finally { + if (!loaded) { + repoLoaded = true; + for (RepoListener listener : listeners) { + listener.onRepoLoaded(); + } } } } @@ -252,40 +251,46 @@ public void loadRemoteReleases(String packageName) { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e(App.TAG, call.request().url() + e.getMessage()); - if (repoUrl.equals(originRepoUrl)) { - repoUrl = backupRepoUrl; - loadRemoteReleases(packageName); - } else if (repoUrl.equals(backupRepoUrl)) { - repoUrl = secondBackupRepoUrl; - loadRemoteReleases(packageName); - } else { - for (RepoListener listener : listeners) { - listener.onThrowable(e); - } + for (RepoListener listener : listeners) { + listener.onThrowable(e); } } @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { + if (!response.isSuccessful()) { + var e = new IOException("Unexpected response " + response.code() + " from " + call.request().url()); + for (RepoListener listener : listeners) { + listener.onThrowable(e); + } + response.close(); + return; + } + try (response) { ResponseBody body = response.body(); - if (body != null) { - try { - String bodyString = body.string(); - Gson gson = new Gson(); - OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); - module.releasesLoaded = true; - onlineModules.replace(packageName, module); - for (RepoListener listener : listeners) { - listener.onModuleReleasesLoaded(module); - } - } catch (Throwable t) { - Log.e(App.TAG, Log.getStackTraceString(t)); - for (RepoListener listener : listeners) { - listener.onThrowable(t); - } + if (body == null) { + throw new IOException("Empty response from " + call.request().url()); + } + try { + String bodyString = body.string(); + Gson gson = new Gson(); + OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); + module.releasesLoaded = true; + onlineModules.replace(packageName, module); + for (RepoListener listener : listeners) { + listener.onModuleReleasesLoaded(module); + } + } catch (Throwable t) { + Log.e(App.TAG, Log.getStackTraceString(t)); + for (RepoListener listener : listeners) { + listener.onThrowable(t); } } + } catch (Throwable t) { + Log.e(App.TAG, Log.getStackTraceString(t)); + for (RepoListener listener : listeners) { + listener.onThrowable(t); + } } } }); diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index 3dcab2aa2..12e280fa2 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -109,6 +109,8 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.RepoLis OnlineModule module; private ReleaseAdapter releaseAdapter; private InformationAdapter informationAdapter; + private boolean remoteModuleLoadRequested = false; + private boolean releaseLoadRequestedByUser = false; @Nullable @Override @@ -147,6 +149,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c releaseAdapter = new ReleaseAdapter(); informationAdapter = new InformationAdapter(); RepoLoader.getInstance().addListener(this); + loadRemoteModuleIfReadmeMissing(); return binding.getRoot(); } @@ -184,7 +187,7 @@ private void renderGithubMarkdown(WebView view, @Nullable String text) { } else { direction = "ltr"; } - if (text == null) { + if (TextUtils.isEmpty(text)) { text = "
" + App.getInstance().getString(R.string.list_empty) + "
"; } if (ResourceUtils.isNightMode(getResources().getConfiguration())) { @@ -238,6 +241,43 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque } } + @Nullable + private OnlineModule refreshModuleFromRepo() { + if (module == null || module.getName() == null) return module; + var updatedModule = RepoLoader.getInstance().getOnlineModule(module.getName()); + if (updatedModule != null) { + module = updatedModule; + } + return module; + } + + private boolean hasReadme(@Nullable OnlineModule module) { + return module != null && (!TextUtils.isEmpty(module.getReadmeHTML()) || !TextUtils.isEmpty(module.getReadme())); + } + + private void loadRemoteModuleIfReadmeMissing() { + var currentModule = refreshModuleFromRepo(); + if (currentModule == null || currentModule.getName() == null) return; + if (remoteModuleLoadRequested || currentModule.releasesLoaded || hasReadme(currentModule)) return; + + remoteModuleLoadRequested = true; + RepoLoader.getInstance().loadRemoteReleases(currentModule.getName()); + } + + @Nullable + private String getModuleReadme() { + var currentModule = refreshModuleFromRepo(); + if (currentModule == null) return null; + String readme = currentModule.getReadmeHTML(); + if (TextUtils.isEmpty(readme)) { + readme = currentModule.getReadme(); + } + if (TextUtils.isEmpty(readme)) { + loadRemoteModuleIfReadmeMissing(); + } + return readme; + } + @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { @@ -260,16 +300,27 @@ public void onDestroyView() { binding = null; } + @Override + public void onRepoLoaded() { + refreshModuleFromRepo(); + loadRemoteModuleIfReadmeMissing(); + if (releaseAdapter != null) { + runAsync(releaseAdapter::loadItems); + } + } + @Override public void onModuleReleasesLoaded(OnlineModule module) { + if (this.module == null || module == null || !TextUtils.equals(this.module.getName(), module.getName())) return; this.module = module; var repoLoader = RepoLoader.getInstance(); if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); } - if ((repoLoader.getReleases(module.getName()) != null ? repoLoader.getReleases(module.getName()).size() : 1) == 1) { + if (releaseLoadRequestedByUser && (repoLoader.getReleases(module.getName()) != null ? repoLoader.getReleases(module.getName()).size() : 1) == 1) { showHint(R.string.module_release_no_more, true); } + releaseLoadRequestedByUser = false; } @Override @@ -456,6 +507,7 @@ public void onBindViewHolder(@NonNull ReleaseAdapter.ViewHolder holder, int posi if (holder.progress.getVisibility() == View.GONE) { holder.title.setVisibility(View.GONE); holder.progress.show(); + releaseLoadRequestedByUser = true; RepoLoader.getInstance().loadRemoteReleases(module.getName()); } }); @@ -611,9 +663,17 @@ public void onPause() { } } - public static class ReadmeFragment extends BorderFragment { + public static class ReadmeFragment extends BorderFragment implements RepoLoader.RepoListener { ItemRepoReadmeBinding binding; + private void renderReadme() { + var parent = getParentFragment(); + if (!(parent instanceof RepoItemFragment) || binding == null) return; + + var repoItemFragment = (RepoItemFragment) parent; + repoItemFragment.renderGithubMarkdown(binding.readme, repoItemFragment.getModuleReadme()); + } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -624,13 +684,40 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } return null; } - var repoItemFragment = (RepoItemFragment) parent; binding = ItemRepoReadmeBinding.inflate(getLayoutInflater(), container, false); - repoItemFragment.renderGithubMarkdown(binding.readme, repoItemFragment.module.getReadmeHTML()); + renderReadme(); borderView = binding.scrollView; + RepoLoader.getInstance().addListener(this); return binding.getRoot(); } + @Override + public void onRepoLoaded() { + if (binding != null) { + runOnUiThread(this::renderReadme); + } + } + + @Override + public void onModuleReleasesLoaded(OnlineModule module) { + if (binding != null) { + var parent = getParentFragment(); + if (parent instanceof RepoItemFragment) { + var repoItemFragment = (RepoItemFragment) parent; + if (repoItemFragment.module != null && TextUtils.equals(repoItemFragment.module.getName(), module.getName())) { + runOnUiThread(this::renderReadme); + } + } + } + } + + @Override + public void onDestroyView() { + RepoLoader.getInstance().removeListener(this); + binding = null; + super.onDestroyView(); + } + @Override void scrollToTop() { binding.scrollView.fullScroll(ScrollView.FOCUS_UP); From 5f247d9a349c1aea7ba20a1fcc0a2529d73ff03e Mon Sep 17 00:00:00 2001 From: Shallow Date: Mon, 22 Jun 2026 12:58:54 +0800 Subject: [PATCH 02/10] Handle repository response edge cases --- .../main/java/org/lsposed/manager/repo/RepoLoader.java | 9 +++++++++ .../lsposed/manager/ui/fragment/RepoItemFragment.java | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index 47798eed1..7530654d6 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -106,6 +106,9 @@ synchronized public void loadRemoteData() { } try { String bodyString = body.string(); + if (bodyString.trim().isEmpty()) { + throw new IOException("Empty response from " + response.request().url()); + } Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); loadLocalData(false); loaded = true; @@ -273,8 +276,14 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { } try { String bodyString = body.string(); + if (bodyString.trim().isEmpty()) { + throw new IOException("Empty response from " + call.request().url()); + } Gson gson = new Gson(); OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); + if (module == null) { + throw new IOException("Invalid response from " + call.request().url()); + } module.releasesLoaded = true; onlineModules.replace(packageName, module); for (RepoListener listener : listeners) { diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index 12e280fa2..dd54df2d8 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -302,7 +302,10 @@ public void onDestroyView() { @Override public void onRepoLoaded() { - refreshModuleFromRepo(); + var currentModule = refreshModuleFromRepo(); + if (!hasReadme(currentModule)) { + remoteModuleLoadRequested = false; + } loadRemoteModuleIfReadmeMissing(); if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); @@ -325,6 +328,8 @@ public void onModuleReleasesLoaded(OnlineModule module) { @Override public void onThrowable(Throwable t) { + remoteModuleLoadRequested = false; + releaseLoadRequestedByUser = false; if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); } From e00bab547255e5b0fa143a7686fb83ca95de6985 Mon Sep 17 00:00:00 2001 From: Shallow Date: Sun, 28 Jun 2026 13:42:04 +0800 Subject: [PATCH 03/10] Restore repository endpoint fallbacks --- .../org/lsposed/manager/repo/RepoLoader.java | 82 ++++++++++++------- .../manager/ui/fragment/RepoItemFragment.java | 7 +- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index 7530654d6..292613790 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -76,7 +76,13 @@ public boolean upgradable(long versionCode, String versionName) { private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json"); private final Set listeners = ConcurrentHashMap.newKeySet(); private boolean repoLoaded = false; - private static final String repoUrl = "https://backup.modules.lsposed.org/"; + private static final String[] repoUrls = new String[]{ + "https://backup.modules.lsposed.org/", + "https://modules.lsposed.org/", + "https://modules-blogcdn.lsposed.org/", + "https://modules-cloudflare.lsposed.org/" + }; + private static String repoUrl = repoUrls[0]; private final Resources resources = App.getInstance().getResources(); private final String[] channels = resources.getStringArray(R.array.update_channel_values); @@ -95,34 +101,25 @@ public static synchronized RepoLoader getInstance() { synchronized public void loadRemoteData() { repoLoaded = false; boolean loaded = false; + Throwable lastError = null; try { - try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(repoUrl + "modules.json").build()).execute()) { - if (!response.isSuccessful()) { - throw new IOException("Unexpected response " + response.code() + " from " + response.request().url()); - } - ResponseBody body = response.body(); - if (body == null) { - throw new IOException("Empty response from " + response.request().url()); - } + for (String candidateRepoUrl : repoUrls) { try { - String bodyString = body.string(); - if (bodyString.trim().isEmpty()) { - throw new IOException("Empty response from " + response.request().url()); - } + String bodyString = requestString(candidateRepoUrl + "modules.json"); Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); + repoUrl = candidateRepoUrl; loadLocalData(false); loaded = true; + break; } catch (Throwable t) { - Log.e(App.TAG, Log.getStackTraceString(t)); - for (RepoListener listener : listeners) { - listener.onThrowable(t); - } + lastError = t; + Log.e(App.TAG, "load remote data from " + candidateRepoUrl, t); } } - } catch (Throwable e) { - Log.e(App.TAG, "load remote data", e); - for (RepoListener listener : listeners) { - listener.onThrowable(e); + if (!loaded && lastError != null) { + for (RepoListener listener : listeners) { + listener.onThrowable(lastError); + } } } finally { if (!loaded) { @@ -134,6 +131,23 @@ synchronized public void loadRemoteData() { } } + private String requestString(String url) throws IOException { + try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(url).build()).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected response " + response.code() + " from " + response.request().url()); + } + ResponseBody body = response.body(); + if (body == null) { + throw new IOException("Empty response from " + response.request().url()); + } + String bodyString = body.string(); + if (bodyString.trim().isEmpty()) { + throw new IOException("Empty response from " + response.request().url()); + } + return bodyString; + } + } + synchronized public void loadLocalData(boolean updateRemoteRepo) { repoLoaded = false; try { @@ -250,23 +264,24 @@ else if (module.getLatestBetaReleaseTime() != null) } public void loadRemoteReleases(String packageName) { - App.getOkHttpClient().newCall(new Request.Builder().url(String.format(repoUrl + "module/%s.json", packageName)).build()).enqueue(new Callback() { + loadRemoteReleases(packageName, 0); + } + + private void loadRemoteReleases(String packageName, int repoUrlIndex) { + String candidateRepoUrl = repoUrls[repoUrlIndex]; + App.getOkHttpClient().newCall(new Request.Builder().url(String.format(candidateRepoUrl + "module/%s.json", packageName)).build()).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e(App.TAG, call.request().url() + e.getMessage()); - for (RepoListener listener : listeners) { - listener.onThrowable(e); - } + retryRemoteReleases(packageName, repoUrlIndex, e); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { var e = new IOException("Unexpected response " + response.code() + " from " + call.request().url()); - for (RepoListener listener : listeners) { - listener.onThrowable(e); - } response.close(); + retryRemoteReleases(packageName, repoUrlIndex, e); return; } try (response) { @@ -286,6 +301,7 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { } module.releasesLoaded = true; onlineModules.replace(packageName, module); + repoUrl = candidateRepoUrl; for (RepoListener listener : listeners) { listener.onModuleReleasesLoaded(module); } @@ -305,6 +321,16 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { }); } + private void retryRemoteReleases(String packageName, int repoUrlIndex, Throwable error) { + if (repoUrlIndex + 1 < repoUrls.length) { + loadRemoteReleases(packageName, repoUrlIndex + 1); + } else { + for (RepoListener listener : listeners) { + listener.onThrowable(error); + } + } + } + public void addListener(RepoListener listener) { listeners.add(listener); } diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index dd54df2d8..a51bfb239 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -302,10 +302,7 @@ public void onDestroyView() { @Override public void onRepoLoaded() { - var currentModule = refreshModuleFromRepo(); - if (!hasReadme(currentModule)) { - remoteModuleLoadRequested = false; - } + refreshModuleFromRepo(); loadRemoteModuleIfReadmeMissing(); if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); @@ -690,9 +687,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return null; } binding = ItemRepoReadmeBinding.inflate(getLayoutInflater(), container, false); - renderReadme(); borderView = binding.scrollView; RepoLoader.getInstance().addListener(this); + renderReadme(); return binding.getRoot(); } From dbb09630ddfe3150411466d32c44aef73c635362 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 01:43:15 +0800 Subject: [PATCH 04/10] Retry module detail mirrors on parse failures --- .../org/lsposed/manager/repo/RepoLoader.java | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index 292613790..1440b20f1 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -289,33 +289,24 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { if (body == null) { throw new IOException("Empty response from " + call.request().url()); } - try { - String bodyString = body.string(); - if (bodyString.trim().isEmpty()) { - throw new IOException("Empty response from " + call.request().url()); - } - Gson gson = new Gson(); - OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); - if (module == null) { - throw new IOException("Invalid response from " + call.request().url()); - } - module.releasesLoaded = true; - onlineModules.replace(packageName, module); - repoUrl = candidateRepoUrl; - for (RepoListener listener : listeners) { - listener.onModuleReleasesLoaded(module); - } - } catch (Throwable t) { - Log.e(App.TAG, Log.getStackTraceString(t)); - for (RepoListener listener : listeners) { - listener.onThrowable(t); - } + String bodyString = body.string(); + if (bodyString.trim().isEmpty()) { + throw new IOException("Empty response from " + call.request().url()); } - } catch (Throwable t) { - Log.e(App.TAG, Log.getStackTraceString(t)); + Gson gson = new Gson(); + OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); + if (module == null) { + throw new IOException("Invalid response from " + call.request().url()); + } + module.releasesLoaded = true; + onlineModules.replace(packageName, module); + repoUrl = candidateRepoUrl; for (RepoListener listener : listeners) { - listener.onThrowable(t); + listener.onModuleReleasesLoaded(module); } + } catch (Throwable t) { + Log.e(App.TAG, Log.getStackTraceString(t)); + retryRemoteReleases(packageName, repoUrlIndex, t); } } }); From d08a313709b64d131599d34e52ce7005827f2971 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 11:06:49 +0800 Subject: [PATCH 05/10] Keep repository mirror fallback robust --- .../org/lsposed/manager/repo/RepoLoader.java | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index 1440b20f1..dc3f28165 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -106,9 +106,10 @@ synchronized public void loadRemoteData() { for (String candidateRepoUrl : repoUrls) { try { String bodyString = requestString(candidateRepoUrl + "modules.json"); + OnlineModule[] repoModules = parseRepoModules(bodyString); Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); repoUrl = candidateRepoUrl; - loadLocalData(false); + replaceRepoModules(repoModules); loaded = true; break; } catch (Throwable t) { @@ -131,6 +132,23 @@ synchronized public void loadRemoteData() { } } + private OnlineModule[] parseRepoModules(String bodyString) throws IOException { + Gson gson = new Gson(); + OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class); + if (repoModules == null) { + throw new IOException("Invalid repo response"); + } + return repoModules; + } + + private void replaceRepoModules(OnlineModule[] repoModules) { + Map modules = new HashMap<>(); + Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule)); + var channel = App.getPreferences().getString("update_channel", channels[0]); + updateLatestVersion(repoModules, channel); + onlineModules = modules; + } + private String requestString(String url) throws IOException { try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(url).build()).execute()) { if (!response.isSuccessful()) { @@ -157,13 +175,8 @@ synchronized public void loadLocalData(boolean updateRemoteRepo) { } byte[] encoded = Files.readAllBytes(repoFile); String bodyString = new String(encoded, StandardCharsets.UTF_8); - Gson gson = new Gson(); - Map modules = new HashMap<>(); - OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class); - Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule)); - var channel = App.getPreferences().getString("update_channel", channels[0]); - updateLatestVersion(repoModules, channel); - onlineModules = modules; + OnlineModule[] repoModules = parseRepoModules(bodyString); + replaceRepoModules(repoModules); } catch (Throwable t) { Log.e(App.TAG, Log.getStackTraceString(t)); for (RepoListener listener : listeners) { @@ -264,16 +277,29 @@ else if (module.getLatestBetaReleaseTime() != null) } public void loadRemoteReleases(String packageName) { - loadRemoteReleases(packageName, 0); + loadRemoteReleases(packageName, repoUrlIndex(repoUrl), 0); + } + + private int repoUrlIndex(String url) { + for (int i = 0; i < repoUrls.length; i++) { + if (repoUrls[i].equals(url)) { + return i; + } + } + return 0; + } + + private int nextRepoUrlIndex(int repoUrlIndex) { + return (repoUrlIndex + 1) % repoUrls.length; } - private void loadRemoteReleases(String packageName, int repoUrlIndex) { + private void loadRemoteReleases(String packageName, int repoUrlIndex, int attempts) { String candidateRepoUrl = repoUrls[repoUrlIndex]; App.getOkHttpClient().newCall(new Request.Builder().url(String.format(candidateRepoUrl + "module/%s.json", packageName)).build()).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e(App.TAG, call.request().url() + e.getMessage()); - retryRemoteReleases(packageName, repoUrlIndex, e); + retryRemoteReleases(packageName, repoUrlIndex, attempts, e); } @Override @@ -281,7 +307,7 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) { var e = new IOException("Unexpected response " + response.code() + " from " + call.request().url()); response.close(); - retryRemoteReleases(packageName, repoUrlIndex, e); + retryRemoteReleases(packageName, repoUrlIndex, attempts, e); return; } try (response) { @@ -306,15 +332,15 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { } } catch (Throwable t) { Log.e(App.TAG, Log.getStackTraceString(t)); - retryRemoteReleases(packageName, repoUrlIndex, t); + retryRemoteReleases(packageName, repoUrlIndex, attempts, t); } } }); } - private void retryRemoteReleases(String packageName, int repoUrlIndex, Throwable error) { - if (repoUrlIndex + 1 < repoUrls.length) { - loadRemoteReleases(packageName, repoUrlIndex + 1); + private void retryRemoteReleases(String packageName, int repoUrlIndex, int attempts, Throwable error) { + if (attempts + 1 < repoUrls.length) { + loadRemoteReleases(packageName, nextRepoUrlIndex(repoUrlIndex), attempts + 1); } else { for (RepoListener listener : listeners) { listener.onThrowable(error); From f4ad0161a395f903c5d0c25a522bed3e62e1f690 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 11:31:30 +0800 Subject: [PATCH 06/10] Notify repo listeners after module map replacement --- app/src/main/java/org/lsposed/manager/repo/RepoLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java index dc3f28165..b2c4efeb2 100644 --- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java +++ b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java @@ -145,8 +145,8 @@ private void replaceRepoModules(OnlineModule[] repoModules) { Map modules = new HashMap<>(); Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule)); var channel = App.getPreferences().getString("update_channel", channels[0]); - updateLatestVersion(repoModules, channel); onlineModules = modules; + updateLatestVersion(repoModules, channel); } private String requestString(String url) throws IOException { From 83553ab56bfb836874c45399c848129ef5a48633 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 11:48:16 +0800 Subject: [PATCH 07/10] Reset readme fetch guard after detail load --- .../java/org/lsposed/manager/ui/fragment/RepoItemFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index a51bfb239..819c532b0 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -313,6 +313,7 @@ public void onRepoLoaded() { public void onModuleReleasesLoaded(OnlineModule module) { if (this.module == null || module == null || !TextUtils.equals(this.module.getName(), module.getName())) return; this.module = module; + remoteModuleLoadRequested = false; var repoLoader = RepoLoader.getInstance(); if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); From 74f60aeb4d7596b7617d772c6f6825b2c8fd2136 Mon Sep 17 00:00:00 2001 From: Shallow Date: Thu, 2 Jul 2026 12:02:12 +0800 Subject: [PATCH 08/10] Clear readme fetch guard on view teardown --- .../java/org/lsposed/manager/ui/fragment/RepoItemFragment.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index 819c532b0..e2c3d69e3 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -297,6 +297,7 @@ public boolean onMenuItemSelected(@NonNull MenuItem item) { public void onDestroyView() { super.onDestroyView(); RepoLoader.getInstance().removeListener(this); + remoteModuleLoadRequested = false; binding = null; } From 145ea213aaa1a68e325226038d19a00bdcf118f5 Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 12:25:30 +0800 Subject: [PATCH 09/10] Keep loaded module detail across repo refreshes --- .../lsposed/manager/ui/fragment/RepoItemFragment.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index e2c3d69e3..ff53d539b 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -246,7 +246,15 @@ private OnlineModule refreshModuleFromRepo() { if (module == null || module.getName() == null) return module; var updatedModule = RepoLoader.getInstance().getOnlineModule(module.getName()); if (updatedModule != null) { - module = updatedModule; + // A repo refresh can replace RepoLoader's entry with the summary + // object from modules.json, which lacks README/release detail that + // was already fetched for this fragment. Keep the richer instance so + // the UI does not flicker back to empty/truncated content. + var currentHasDetail = module.releasesLoaded || hasReadme(module); + var updatedHasDetail = updatedModule.releasesLoaded || hasReadme(updatedModule); + if (!currentHasDetail || updatedHasDetail) { + module = updatedModule; + } } return module; } From 7db9055b44833be7dcee56afd52e02e0326488a8 Mon Sep 17 00:00:00 2001 From: Shallow Date: Sat, 4 Jul 2026 18:30:02 +0800 Subject: [PATCH 10/10] Prefer loaded module releases over repo summary --- .../org/lsposed/manager/ui/fragment/RepoItemFragment.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java index ff53d539b..edfdcf16b 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java @@ -479,7 +479,12 @@ public ReleaseAdapter() { public void loadItems() { var channels = resources.getStringArray(R.array.update_channel_values); var channel = App.getPreferences().getString("update_channel", channels[0]); - var releases = RepoLoader.getInstance().getReleases(module.getName()); + // Prefer this fragment's module when its releases were already loaded + // in full; a repo refresh may have replaced RepoLoader's entry with + // the modules.json summary, whose truncated release list would + // shadow the complete data we already fetched. + List releases = module.releasesLoaded ? module.getReleases() : null; + if (releases == null) releases = RepoLoader.getInstance().getReleases(module.getName()); if (releases == null) releases = module.getReleases(); List tmpList; if (channel.equals(channels[0])) {