diff --git a/app/build.gradle b/app/build.gradle index 914e0dc..b07c182 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.fox2code.mmm" minSdk 21 targetSdk 31 - versionCode 13 - versionName "0.2.3" + versionCode 14 + versionName "0.2.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -50,8 +50,8 @@ configurations { dependencies { // UI implementation 'androidx.appcompat:appcompat:1.4.0' - implementation 'androidx.emoji2:emoji2:1.0.0' - implementation 'androidx.emoji2:emoji2-views-helper:1.0.0' + implementation 'androidx.emoji2:emoji2:1.0.1' + implementation 'androidx.emoji2:emoji2-views-helper:1.0.1' implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.recyclerview:recyclerview:1.2.1' diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index bbf516c..624d1cd 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -64,6 +64,12 @@ public class MainApplication extends Application implements CompatActivity.Appli secret = new Random().nextInt(); } + public MainApplication() { + if (INSTANCE != null) + throw new IllegalStateException("Duplicate application instance!"); + INSTANCE = this; + } + public static Shell build(String... command) { return shellBuilder.build(command); } @@ -167,7 +173,7 @@ public class MainApplication extends Application implements CompatActivity.Appli .usePlugin(SyntaxHighlightPlugin.create( new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme())) .usePlugin(ImagesPlugin.create().addSchemeHandler( - OkHttpNetworkSchemeHandler.create(Http.getHttpclientWithCache()))).build(); + OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build(); return this.markwon = markwon; } @@ -243,7 +249,6 @@ public class MainApplication extends Application implements CompatActivity.Appli @Override public void onCreate() { - INSTANCE = this; super.onCreate(); SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); // We are only one process so it's ok to do this diff --git a/app/src/main/java/com/fox2code/mmm/utils/Http.java b/app/src/main/java/com/fox2code/mmm/utils/Http.java index ea4e42c..98e8141 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -1,5 +1,7 @@ package com.fox2code.mmm.utils; +import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; @@ -13,9 +15,11 @@ import java.io.InputStream; import java.net.InetAddress; import java.net.Proxy; import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -34,6 +38,7 @@ import okhttp3.brotli.BrotliInterceptor; import okhttp3.dnsoverhttps.DnsOverHttps; public class Http { + private static final String TAG = "Http"; private static final OkHttpClient httpClient; private static final OkHttpClient httpClientWithCache; @@ -70,28 +75,34 @@ public class Http { Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))) .bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build(); } catch (UnknownHostException|RuntimeException e) { - Log.e("Http", "Failed to init DoH", e); + Log.e(TAG, "Failed to init DoH", e); } httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); - httpclientBuilder.dns(dns); - httpClient = httpclientBuilder.build(); MainApplication mainApplication = MainApplication.getINSTANCE(); if (mainApplication != null) { + httpclientBuilder.dns(new FallBackDNS(mainApplication, dns, "github.com", + "api.github.com", "raw.githubusercontent.com", "camo.githubusercontent.com", + "user-images.githubusercontent.com", "cdn.jsdelivr.net", "img.shields.io", + "magisk-modules-repo.github.io", "www.androidacy.com")); + httpClient = httpclientBuilder.build(); httpclientBuilder.cache(new Cache( new File(mainApplication.getCacheDir(), "http_cache"), 2L * 1024L * 1024L)); // 2Mib of cache httpclientBuilder.cookieJar(new CDNCookieJar()); httpClientWithCache = httpclientBuilder.build(); + Log.i(TAG, "Initialized Http successfully!"); } else { - httpClientWithCache = httpClient; + httpclientBuilder.dns(dns); + httpClientWithCache = httpClient = httpclientBuilder.build(); + Log.e(TAG, "Initialized Http too soon!"); } } - public static OkHttpClient getHttpclientNoCache() { + public static OkHttpClient getHttpClient() { return httpClient; } - public static OkHttpClient getHttpclientWithCache() { + public static OkHttpClient getHttpClientWithCache() { return httpClientWithCache; } @@ -207,4 +218,85 @@ public class Http { public interface ProgressListener { void onUpdate(int downloaded,int total, boolean done); } + + /** + * FallBackDNS store successful DNS request to return them later + * can help make the app to work later when the current DNS system + * isn't functional or available. + * + * Note: DNS Cache is stored in user data. + * */ + private static class FallBackDNS implements Dns { + private final Dns parent; + private final SharedPreferences sharedPreferences; + private final HashSet fallbacks; + private final HashMap> fallbackCache; + + public FallBackDNS(Context context, Dns parent, String... fallbacks) { + this.sharedPreferences = context.getSharedPreferences( + "mmm_dns", Context.MODE_PRIVATE); + this.parent = parent; + this.fallbacks = new HashSet<>(Arrays.asList(fallbacks)); + this.fallbackCache = new HashMap<>(); + } + + @NonNull + @Override + public List lookup(@NonNull String s) throws UnknownHostException { + if (this.fallbacks.contains(s)) { + List addresses = this.fallbackCache.get(s); + if (addresses != null) + return addresses; + try { + addresses = this.parent.lookup(s); + if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress()) + throw new UnknownHostException(s); + this.fallbackCache.put(s, addresses); + this.sharedPreferences.edit().putString( + s.replace('.', '_'), toString(addresses)).apply(); + } catch (UnknownHostException e) { + String key = this.sharedPreferences.getString( + s.replace('.', '_'), ""); + if (!key.isEmpty()) try { + addresses = fromString(key); + this.fallbackCache.put(s, addresses); + return addresses; + } catch (UnknownHostException e2) { + this.sharedPreferences.edit().remove( + s.replace('.', '_')).apply(); + } + throw e; + } + + return addresses; + } else { + return this.parent.lookup(s); + } + } + + @NonNull + private static String toString(@NonNull List inetAddresses) { + if (inetAddresses.isEmpty()) return ""; + Iterator inetAddressIterator = inetAddresses.iterator(); + StringBuilder stringBuilder = new StringBuilder(); + while (true) { + stringBuilder.append(inetAddressIterator.next().getHostAddress()); + if (!inetAddressIterator.hasNext()) + return stringBuilder.toString(); + stringBuilder.append("|"); + } + } + + @NonNull + private static List fromString(@NonNull String string) + throws UnknownHostException { + if (string.isEmpty()) return Collections.emptyList(); + String[] strings = string.split("\\|"); + ArrayList inetAddresses = new ArrayList<>(strings.length); + for (String address : strings) { + inetAddresses.add(InetAddress.getByName(address)); + } + return inetAddresses; + } + } }