Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/src/main/assets/webview/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,13 @@ input[type="checkbox"] {
color: var(--red-600);
}

/* GitHub now emits the heading permalink anchor as a sibling AFTER the
heading (not a child), so the old ".../h6 .octicon-link" hide rule no
longer matches it and the floated icon leaks into the left gutter of the
following block. These permalinks are useless in this viewer (no hover,
no address bar), so hide them outright. */
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
display: none;
}

.markdown-body .anchor:focus {
Expand Down
266 changes: 202 additions & 64 deletions app/src/main/java/org/lsposed/manager/repo/RepoLoader.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ private static class CompileTask extends AsyncTask<String, Void, Throwable> {
@Override
protected Throwable doInBackground(String... commands) {
try {
LSPManagerServiceHolder.getService().clearApplicationProfileData(commands[0]);
if (LSPManagerServiceHolder.getService().performDexOptMode(commands[0])) {
if (LSPManagerServiceHolder.getService().optimizePackage(commands[0])) {
return null;
} else {
return new UnknownError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}

Expand All @@ -157,6 +160,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {

String modulePackageName = getArguments() == null ? null : getArguments().getString("modulePackageName");
module = RepoLoader.getInstance().getOnlineModule(modulePackageName);
Log.i(App.TAG, "RepoItem: open " + modulePackageName + " -> module " + (module == null ? "NOT FOUND (repoLoaded=" + RepoLoader.getInstance().isRepoLoaded() + "), navigating back" : "found"));
if (module == null) {
if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) {
safeNavigate(R.id.repo_nav);
Expand Down Expand Up @@ -184,7 +188,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())) {
Expand Down Expand Up @@ -238,6 +242,58 @@ 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) {
// 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;
}

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

// True while the per-module detail (which carries the README) is still being
// fetched, so the README tab can show a loading state instead of the empty
// placeholder on a slow connection.
private boolean isModuleDetailLoading() {
return remoteModuleLoadRequested;
}

@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) {

Expand All @@ -257,23 +313,38 @@ public boolean onMenuItemSelected(@NonNull MenuItem item) {
public void onDestroyView() {
super.onDestroyView();
RepoLoader.getInstance().removeListener(this);
remoteModuleLoadRequested = false;
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;
remoteModuleLoadRequested = false;
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);
}
Expand Down Expand Up @@ -416,7 +487,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<Release> releases = module.releasesLoaded ? module.getReleases() : null;
if (releases == null) releases = RepoLoader.getInstance().getReleases(module.getName());
if (releases == null) releases = module.getReleases();
List<Release> tmpList;
if (channel.equals(channels[0])) {
Expand All @@ -431,8 +507,9 @@ public void loadItems() {
return !(name != null && name.startsWith("snapshot")) && !(name != null && name.startsWith("nightly"));
}).collect(Collectors.toList()) : null;
} else tmpList = releases;
List<Release> newItems = tmpList != null ? tmpList : new ArrayList<>();
runOnUiThread(() -> {
items = tmpList;
items = newItems;
notifyDataSetChanged();
});
}
Expand All @@ -456,6 +533,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());
}
});
Expand Down Expand Up @@ -611,8 +689,42 @@ public void onPause() {
}
}

public static class ReadmeFragment extends BorderFragment {
public static class ReadmeFragment extends BorderFragment implements RepoLoader.RepoListener {
ItemRepoReadmeBinding binding;
private String renderedReadme;
private boolean readmeRendered = false;

private void renderReadme() {
var parent = getParentFragment();
if (!(parent instanceof RepoItemFragment) || binding == null) return;

var repoItemFragment = (RepoItemFragment) parent;
// getModuleReadme() also kicks off the per-module fetch when the
// README is missing, so query the loading state afterwards.
var readme = repoItemFragment.getModuleReadme();
String display;
if (!TextUtils.isEmpty(readme)) {
display = readme;
} else if (repoItemFragment.isModuleDetailLoading()) {
// Detail is still downloading (e.g. slow connection); show a
// loading placeholder rather than the empty state so users are
// not misled into thinking the module has no README.
display = "<center>" + getString(R.string.loading) + "</center>";
} else {
// Detail has loaded and there is genuinely no README; let
// renderGithubMarkdown fall back to the empty placeholder.
display = null;
}
var pkg = repoItemFragment.module == null ? null : repoItemFragment.module.getName();
Log.i(App.TAG, "RepoItem: render README for " + pkg + " -> " + (!TextUtils.isEmpty(readme) ? "content" : repoItemFragment.isModuleDetailLoading() ? "loading" : "empty"));
// onRepoLoaded fires on every repo load and channel change; skip the
// WebView reload when the rendered content has not actually changed
// to avoid flicker.
if (readmeRendered && TextUtils.equals(renderedReadme, display)) return;
renderedReadme = display;
readmeRendered = true;
repoItemFragment.renderGithubMarkdown(binding.readme, display);
}

@Nullable
@Override
Expand All @@ -624,13 +736,52 @@ 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 onThrowable(Throwable t) {
// The fetch failed; re-render so the tab leaves the loading state
// (the parent already reset the in-flight flag before this runnable
// executes) instead of spinning forever.
if (binding != null) {
runOnUiThread(this::renderReadme);
}
}

@Override
public void onDestroyView() {
RepoLoader.getInstance().removeListener(this);
binding = null;
renderedReadme = null;
readmeRendered = false;
super.onDestroyView();
}

@Override
void scrollToTop() {
binding.scrollView.fullScroll(ScrollView.FOCUS_UP);
Expand Down
27 changes: 23 additions & 4 deletions app/src/main/java/org/lsposed/manager/util/CloudflareDNS.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lsposed.manager.util;

import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;

Expand All @@ -10,6 +11,7 @@
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.List;

import okhttp3.ConnectionSpec;
Expand All @@ -25,6 +27,10 @@ public final class CloudflareDNS implements Dns {
public boolean DoH = App.getPreferences().getBoolean("doh", false);
public boolean noProxy = ProxySelector.getDefault().select(url.uri()).get(0) == Proxy.NO_PROXY;
private final Dns cloudflare;
// Set once the DoH resolver proves unreachable (e.g. Cloudflare blocked on
// this network) so we stop paying its timeout on every subsequent lookup and
// use the system resolver for the rest of the session.
private volatile boolean dohUnavailable = false;

public CloudflareDNS() {
var trustManager = Platform.get().platformTrustManager();
Expand All @@ -42,6 +48,11 @@ public CloudflareDNS() {
.cache(App.getOkHttpCache())
.sslSocketFactory(new NoSniFactory(), trustManager)
.connectionSpecs(List.of(tls))
// Fail fast when the DoH endpoint is blocked so the
// system-DNS fallback kicks in quickly instead of
// stalling on the default 10s connect timeout.
.connectTimeout(Duration.ofSeconds(3))
.callTimeout(Duration.ofSeconds(5))
.build());
try {
builder.bootstrapDnsHosts(List.of(
Expand All @@ -57,10 +68,18 @@ public CloudflareDNS() {
@NonNull
@Override
public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
if (DoH && noProxy) {
return cloudflare.lookup(hostname);
} else {
return SYSTEM.lookup(hostname);
if (DoH && noProxy && !dohUnavailable) {
try {
return cloudflare.lookup(hostname);
} catch (UnknownHostException e) {
// The DoH resolver is unreachable on this network (e.g. Cloudflare
// is blocked). Fall back to the system resolver so the app keeps
// working instead of failing every lookup, and skip DoH for the
// rest of the session.
dohUnavailable = true;
Log.w(App.TAG, "DoH resolver unreachable, falling back to system DNS for this session: " + e.getMessage());
}
}
return SYSTEM.lookup(hostname);
}
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ subprojects {
val flags =
listOf(
"-DVERSION_CODE=${versionCodeProvider.get()}",
"-DVERSION_NAME='\"${versionNameProvider.get()}\"'",
"-DVERSION_NAME=${versionNameProvider.get()}",
)

val args =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ import java.io.FileInputStream
import java.nio.file.Files
import java.nio.file.Paths
import kotlinx.coroutines.launch
import org.lsposed.lspd.ILSPManagerService
import org.matrix.vector.daemon.VectorDaemon

private const val TAG = "VectorDex2Oat"

// Compatibility states matching Manager expectations
const val DEX2OAT_OK = 0
const val DEX2OAT_MOUNT_FAILED = 1
const val DEX2OAT_SEPOLICY_INCORRECT = 2
const val DEX2OAT_SELINUX_PERMISSIVE = 3
const val DEX2OAT_CRASHED = 4
// Compatibility states mirrored directly from the ILSPManagerService AIDL contract.
val DEX2OAT_OK = ILSPManagerService.DEX2OAT_OK
val DEX2OAT_MOUNT_FAILED = ILSPManagerService.DEX2OAT_MOUNT_FAILED
val DEX2OAT_SEPOLICY_INCORRECT = ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT
val DEX2OAT_SELINUX_PERMISSIVE = ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE
val DEX2OAT_CRASHED = ILSPManagerService.DEX2OAT_CRASHED

object Dex2OatServer {
private const val WRAPPER32 = "bin/dex2oat32"
Expand Down
Loading
Loading