Skip to content
191 changes: 124 additions & 67 deletions app/src/main/java/org/lsposed/manager/repo/RepoLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ public boolean upgradable(long versionCode, String versionName) {
private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json");
private final Set<RepoListener> 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[] 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);

Expand All @@ -98,37 +100,69 @@ 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()) {
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);
}
}
}
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;
replaceRepoModules(repoModules);
loaded = true;
break;
} catch (Throwable 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);
}
}
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();
}
}
}
}

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<String, OnlineModule> 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Notify listeners after replacing module map

In the successful loadRemoteData() path, replaceRepoModules() now relies on updateLatestVersion() to fire onRepoLoaded(), but that notification happens before onlineModules is swapped. During the normal startup background refresh (loadLocalData(true) -> loadRemoteData()), listeners such as RepoFragment.refresh() and RepoItemFragment.refreshModuleFromRepo() read the old cached map and there is no later success notification after line 149, so the UI can keep showing stale module data until another manual refresh. Assign the new map before notifying, or emit a post-assignment onRepoLoaded() for successful remote loads.

Useful? React with 👍 / 👎.

}

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;
}
}

Expand All @@ -141,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<String, OnlineModule> 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) {
Expand Down Expand Up @@ -248,49 +277,77 @@ 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, 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, 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());
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);
}
}
retryRemoteReleases(packageName, repoUrlIndex, attempts, 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());
response.close();
retryRemoteReleases(packageName, repoUrlIndex, attempts, e);
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());
}
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));
retryRemoteReleases(packageName, repoUrlIndex, attempts, t);
}
}
});
}

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);
}
}
}

public void addListener(RepoListener listener) {
listeners.add(listener);
}
Expand Down
Loading