-
Notifications
You must be signed in to change notification settings - Fork 2
Fork review for upstream #765: module repository loading #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
a5efce5
5f247d9
e00bab5
dbb0963
d08a313
f4ad016
83553ab
74f60ae
145ea21
7db9055
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
|
||
|
|
@@ -98,37 +100,51 @@ 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"); | ||
| Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); | ||
| repoUrl = candidateRepoUrl; | ||
| loadLocalData(false); | ||
| 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 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; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -248,49 +264,64 @@ 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After the repo index or a previous detail request succeeds on a fallback mirror, Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| 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()); | ||
| 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, 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, 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, t); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = "<center>" + App.getInstance().getString(R.string.list_empty) + "</center>"; | ||
| } | ||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a module detail request has already populated Useful? React with 👍 / 👎. |
||
| } | ||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the automatic detail fetch succeeds, Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the automatic detail fetch is started and the fragment view is destroyed before the OkHttp callback runs, Useful? React with 👍 / 👎. |
||
|
|
||
| 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,20 +300,33 @@ public void onDestroyView() { | |
| binding = null; | ||
| } | ||
|
|
||
| @Override | ||
| public void onRepoLoaded() { | ||
| refreshModuleFromRepo(); | ||
| loadRemoteModuleIfReadmeMissing(); | ||
| if (releaseAdapter != null) { | ||
| runAsync(releaseAdapter::loadItems); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a repo refresh arrives after this fragment has already fetched full module details, Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a repo refresh arrives after this fragment has already loaded full module details, Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| @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 | ||
| public void onThrowable(Throwable t) { | ||
| remoteModuleLoadRequested = false; | ||
| releaseLoadRequestedByUser = false; | ||
| if (releaseAdapter != null) { | ||
| runAsync(releaseAdapter::loadItems); | ||
| } | ||
|
|
@@ -456,6 +509,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 +665,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 +686,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()); | ||
| borderView = binding.scrollView; | ||
| RepoLoader.getInstance().addListener(this); | ||
| renderReadme(); | ||
| 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); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a mirror returns HTTP 200 with a malformed or incompatible
modules.json,loadLocalData(false)catches the Gson/stream exception internally, so execution returns here andloadedis set to true. That stops the new fallback loop before trying the next mirrors and leaves the just-written bad response inrepo.json, so users whose first mirror has a bad cached/deployed response can lose the cached repo instead of falling back tomodules.lsposed.org.Useful? React with 👍 / 👎.