diff --git a/app/build.gradle b/app/build.gradle index dab0edc..5ff7f84 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ plugins { // Gradle doesn't allow conditionally enabling/disabling plugins - id "io.sentry.android.gradle" version "3.1.5" + id "io.sentry.android.gradle" version "3.3.0" id 'com.android.application' id 'com.mikepenz.aboutlibraries.plugin' } @@ -8,13 +8,14 @@ plugins { android { namespace "com.fox2code.mmm" compileSdk 33 + buildToolsVersion '30.0.3' defaultConfig { applicationId "com.fox2code.mmm" minSdk 21 targetSdk 33 - versionCode 59 - versionName "0.6.7" + versionCode 60 + versionName "0.6.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -28,9 +29,9 @@ android { applicationIdSuffix '.debug' debuggable true // ONLY FOR TESTING SENTRY - // minifyEnabled true - // shrinkResources true - // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -137,7 +138,7 @@ sentry { // as Gradle will resolve it to the latest version. // // Defaults to the latest published sentry version. - sentryVersion = '6.5.0' + sentryVersion = '6.8.0' } } @@ -175,17 +176,18 @@ dependencies { implementation 'androidx.work:work-runtime:2.7.1' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10' implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.10' + implementation 'com.google.net.cronet:cronet-okhttp:0.1.0' implementation 'com.github.topjohnwu.libsu:io:5.0.1' implementation 'com.github.Fox2Code:RosettaX:1.0.9' implementation 'com.github.Fox2Code:AndroidANSI:1.0.1' if (hasSentryConfig) { // Error reporting - defaultImplementation 'io.sentry:sentry-android:6.5.0' - defaultImplementation 'io.sentry:sentry-android-fragment:6.5.0' - defaultImplementation 'io.sentry:sentry-android-okhttp:6.5.0' - defaultImplementation 'io.sentry:sentry-android-core:6.5.0' - defaultImplementation 'io.sentry:sentry-android-ndk:6.5.0' + defaultImplementation 'io.sentry:sentry-android:6.8.0' + defaultImplementation 'io.sentry:sentry-android-fragment:6.8.0' + defaultImplementation 'io.sentry:sentry-android-okhttp:6.8.0' + defaultImplementation 'io.sentry:sentry-android-core:6.8.0' + defaultImplementation 'io.sentry:sentry-android-ndk:6.8.0' } // Markdown @@ -193,13 +195,13 @@ dependencies { implementation "io.noties.markwon:html:4.6.2" implementation "io.noties.markwon:image:4.6.2" implementation "io.noties.markwon:syntax-highlight:4.6.2" + implementation 'com.google.android.gms:play-services-cronet:18.0.1' annotationProcessor "io.noties:prism4j-bundler:2.0.0" implementation "com.caverock:androidsvg:1.4" // Test testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' } if (hasSentryConfig) { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2a40c08..0d82679 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -185,4 +185,4 @@ int getSafeInsetRight(); int getSafeInsetTop(); android.graphics.Insets getWaterfallInsets(); -} +} \ No newline at end of file diff --git a/app/src/main/assets/module_installer_compat.sh b/app/src/main/assets/module_installer_compat.sh index 07ef0ab..726912b 100644 --- a/app/src/main/assets/module_installer_compat.sh +++ b/app/src/main/assets/module_installer_compat.sh @@ -1,4 +1,4 @@ -#!/sbin/sh +# shellcheck shell=ash ################# # Initialization @@ -20,24 +20,24 @@ require_new_magisk() { # Load util_functions.sh ######################### -OUTFD=$2 -ZIPFILE=$3 +export OUTFD=$2 +export ZIPFILE=$3 mount /data 2>/dev/null [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk . /data/adb/magisk/util_functions.sh -[ $MAGISK_VER_CODE -lt 19000 ] && require_new_magisk +[ "$MAGISK_VER_CODE" -lt 19000 ] && require_new_magisk # Add grep_get_prop implementation if missing if ! type grep_get_prop &>/dev/null; then grep_get_prop() { - local result=$(grep_prop $@) + local result=$(grep_prop "$@") if [ -z "$result" ]; then # Fallback to getprop getprop "$1" else - echo $result + echo "$result" fi } fi @@ -55,7 +55,7 @@ settings() { fi } -if [ $MAGISK_VER_CODE -ge 20400 ] && [ -z "$MMM_MMT_REBORN" ]; then +if [ "$MAGISK_VER_CODE" -ge 20400 ] && [ -z "$MMM_MMT_REBORN" ]; then # New Magisk have complete installation logic within util_functions.sh install_module exit 0 @@ -75,9 +75,10 @@ is_legacy_script() { print_modname() { local authlen len namelen pounds - namelen=`echo -n $MODNAME | wc -c` - authlen=$((`echo -n $MODAUTH | wc -c` + 3)) - [ $namelen -gt $authlen ] && len=$namelen || len=$authlen + # shellcheck disable=SC2006 + namelen=`echo -n "$MODNAME" | wc -c` + authlen=$(($(echo -n "$MODAUTH" | wc -c) + 3)) + [ "$namelen" -gt $authlen ] && len=$namelen || len=$authlen len=$((len + 2)) pounds=$(printf "%${len}s" | tr ' ' '*') ui_print "$pounds" @@ -93,7 +94,7 @@ print_modname() { abort() { ui_print "$1" $BOOTMODE || recovery_cleanup - [ -n $MODPATH ] && rm -rf $MODPATH + [ -n "$MODPATH" ] && rm -rf "$MODPATH" rm -rf $TMPDIR exit 1 } @@ -101,7 +102,7 @@ abort() { rm -rf $TMPDIR 2>/dev/null mkdir -p $TMPDIR chcon u:object_r:system_file:s0 $TMPDIR || true -cd $TMPDIR +cd $TMPDIR || exit # Preperation for flashable zips setup_flashable @@ -128,14 +129,15 @@ unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2 MODDIRNAME=modules $BOOTMODE && MODDIRNAME=modules_update MODULEROOT=$NVBASE/$MODDIRNAME -MODID=`grep_prop id $TMPDIR/module.prop` -MODNAME=`grep_prop name $TMPDIR/module.prop` -MODAUTH=`grep_prop author $TMPDIR/module.prop` +MODID=$(grep_prop id $TMPDIR/module.prop) +MODNAME=$(grep_prop name $TMPDIR/module.prop) +MODAUTH=$(grep_prop author $TMPDIR/module.prop) MODPATH=$MODULEROOT/$MODID # Create mod paths +# shellcheck disable=SC2086 rm -rf $MODPATH 2>/dev/null -mkdir -p $MODPATH +mkdir -p "$MODPATH" ########## # Install @@ -152,22 +154,22 @@ if is_legacy_script; then on_install # Custom uninstaller - [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh + [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh "$MODPATH"/uninstall.sh # Skip mount - $SKIPMOUNT && touch $MODPATH/skip_mount + $SKIPMOUNT && touch "$MODPATH"/skip_mount # prop file - $PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop + $PROPFILE && cp -af $TMPDIR/system.prop "$MODPATH"/system.prop # Module info - cp -af $TMPDIR/module.prop $MODPATH/module.prop + cp -af $TMPDIR/module.prop "$MODPATH"/module.prop # post-fs-data scripts - $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh + $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh "$MODPATH"/post-fs-data.sh # service scripts - $LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh + $LATESTARTSERVICE && cp -af $TMPDIR/service.sh "$MODPATH"/service.sh ui_print "- Setting permissions" set_permissions @@ -218,46 +220,46 @@ elif [ -n "$MMM_MMT_REBORN" ]; then else print_modname - unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2 + unzip -o "$ZIPFILE" customize.sh -d "$MODPATH" >&2 - if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then + if ! grep -q '^SKIPUNZIP=1$' "$MODPATH"/customize.sh 2>/dev/null; then ui_print "- Extracting module files" - unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2 + unzip -o "$ZIPFILE" -x 'META-INF/*' -d "$MODPATH" >&2 # Default permissions - set_perm_recursive $MODPATH 0 0 0755 0644 + set_perm_recursive "$MODPATH" 0 0 0755 0644 fi # Load customization script - [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh + [ -f "$MODPATH"/customize.sh ] && . "$MODPATH"/customize.sh fi # Handle replace folders for TARGET in $REPLACE; do ui_print "- Replace target: $TARGET" - mktouch $MODPATH$TARGET/.replace + mktouch "$MODPATH""$TARGET"/.replace done if $BOOTMODE; then # Update info for Magisk Manager - mktouch $NVBASE/modules/$MODID/update - rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null - rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null - cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop + mktouch $NVBASE/modules/"$MODID"/update + rm -rf $NVBASE/modules/"$MODID"/remove 2>/dev/null + rm -rf $NVBASE/modules/"$MODID"/disable 2>/dev/null + cp -af "$MODPATH"/module.prop $NVBASE/modules/"$MODID"/module.prop fi # Copy over custom sepolicy rules if ! type copy_sepolicy_rules &>/dev/null; then - if [ -f $MODPATH/sepolicy.rule -a -e $PERSISTDIR ]; then + if [ -f "$MODPATH"/sepolicy.rule -a -e $PERSISTDIR ]; then ui_print "- Installing custom sepolicy patch" # Remove old recovery logs (which may be filling partition) to make room rm -f $PERSISTDIR/cache/recovery/* PERSISTMOD=$PERSISTDIR/magisk/$MODID - mkdir -p $PERSISTMOD - cp -af $MODPATH/sepolicy.rule $PERSISTMOD/sepolicy.rule || abort "! Insufficient partition size" + mkdir -p "$PERSISTMOD" + cp -af "$MODPATH"/sepolicy.rule "$PERSISTMOD"/sepolicy.rule || abort "! Insufficient partition size" fi else - if [ -f $MODPATH/sepolicy.rule ]; then + if [ -f "$MODPATH"/sepolicy.rule ]; then ui_print "- Installing custom sepolicy rules" copy_sepolicy_rules fi @@ -265,9 +267,9 @@ fi # Remove stuff that doesn't belong to modules and clean up any empty directories rm -rf \ -$MODPATH/system/placeholder $MODPATH/customize.sh \ -$MODPATH/README.md $MODPATH/.git* 2>/dev/null -rmdir -p $MODPATH +"$MODPATH"/system/placeholder "$MODPATH"/customize.sh \ +"$MODPATH"/README.md "$MODPATH"/.git* 2>/dev/null +rmdir -p "$MODPATH" ############# # Finalizing diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index e70319e..5133a51 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -2,6 +2,7 @@ package com.fox2code.mmm; import android.annotation.SuppressLint; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; @@ -65,6 +66,8 @@ public class MainApplication extends FoxApplication private static String relPackageName = BuildConfig.APPLICATION_ID; private static MainApplication INSTANCE; private static boolean firstBoot; + // Provides the Context for the base application + public Context FoxApplication = this; static { Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create() diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java index 6da67d6..7f4a1b5 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -74,8 +74,7 @@ public final class AndroidacyActivity extends FoxActivity { super.onCreate(savedInstanceState); Intent intent = this.getIntent(); Uri uri; - if (!MainApplication.checkSecret(intent) || - (uri = intent.getData()) == null) { + if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) { Log.w(TAG, "Impersonation detected"); this.forceBackPressed(); return; @@ -99,8 +98,14 @@ public final class AndroidacyActivity extends FoxActivity { url = url + '?' + AndroidacyUtil.REFERRER; } } - boolean allowInstall = intent.getBooleanExtra( - Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false); + // Add token to url if not present + String token = uri.getQueryParameter("token"); + if (token == null) { + // get from shared preferences + token = MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", null); + url = url + "&token=" + token; + } + boolean allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false); String title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE); String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG); int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0); @@ -119,11 +124,10 @@ public final class AndroidacyActivity extends FoxActivity { String configPkg = IntentHelper.getPackageOfConfig(config); try { XHooks.checkConfigTargetExists(this, configPkg, config); - this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, - menu -> { - IntentHelper.openConfig(this, config); - return true; - }); + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> { + IntentHelper.openConfig(this, config); + return true; + }); } catch (PackageManager.NameNotFoundException ignored) { } } @@ -142,8 +146,7 @@ public final class AndroidacyActivity extends FoxActivity { webSettings.setAllowContentAccess(false); // Attempt at fixing CloudFlare captcha. if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) { - WebSettingsCompat.setRequestedWithHeaderMode( - webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER); + WebSettingsCompat.setRequestedWithHeaderMode(webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER); } // If API level is .= 33, allow setAlgorithmicDarkeningAllowed if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { @@ -153,23 +156,19 @@ public final class AndroidacyActivity extends FoxActivity { } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme - webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? - WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON); + webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON); } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // If api level is < 32, use force dark - WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? - WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON); + WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON); } } this.webView.setWebViewClient(new WebViewClientCompat() { private String pageUrl; @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { + public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { // Don't open non Androidacy urls inside WebView - if (request.isForMainFrame() && - !AndroidacyUtil.isAndroidacyLink(request.getUrl())) { + if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) { if (downloadMode || backOnResume) return true; Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail. AndroidacyUtil.hideToken(request.getUrl().toString())); @@ -181,13 +180,10 @@ public final class AndroidacyActivity extends FoxActivity { @Nullable @Override - public WebResourceResponse shouldInterceptRequest( - WebView view, WebResourceRequest request) { - if (AndroidacyActivity.this.megaIntercept( - this.pageUrl, request.getUrl().toString())) { + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) { // Block request as Androidacy doesn't allow duplicate requests - return new WebResourceResponse("text/plain", "UTF-8", - new ByteArrayInputStream(new byte[0])); + return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0])); } return null; } @@ -205,15 +201,11 @@ public final class AndroidacyActivity extends FoxActivity { } private void onReceivedError(String url, int errorCode) { - if ((url.startsWith("https://production-api.androidacy.com/magisk/") || - url.startsWith("https://staging-api.androidacy.com/magisk/") || - url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) { - Toast.makeText(AndroidacyActivity.this, - "Too many requests!", Toast.LENGTH_LONG).show(); + if ((url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith("https://staging-api.androidacy.com/magisk/") || url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) { + Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show(); AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed); } else if (url.equals(this.pageUrl)) { - postOnUiThread(() -> - webViewNote.setVisibility(View.VISIBLE)); + postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE)); } } @@ -223,8 +215,7 @@ public final class AndroidacyActivity extends FoxActivity { } @Override - public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { + public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) { if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { this.onReceivedError(request.getUrl().toString(), error.getErrorCode()); } @@ -232,12 +223,8 @@ public final class AndroidacyActivity extends FoxActivity { }); this.webView.setWebChromeClient(new WebChromeClient() { @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, - FileChooserParams fileChooserParams) { - FoxActivity.getFoxActivity(webView).startActivityForResult( - fileChooserParams.createIntent(), (code, data) -> - filePathCallback.onReceiveValue( - FileChooserParams.parseResult(code, data))); + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data))); return true; } @@ -277,62 +264,31 @@ public final class AndroidacyActivity extends FoxActivity { progressIndicator.setVisibility(View.INVISIBLE); } }); - this.webView.setDownloadListener(( - downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> { + this.webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> { if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return; if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) { AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; if (androidacyWebAPI != null) { if (!androidacyWebAPI.downloadMode) { // Native module popup may cause download after consumed action - if (androidacyWebAPI.consumedAction) - return; + if (androidacyWebAPI.consumedAction) return; // Workaround Androidacy bug final String moduleId = moduleIdOfUrl(downloadUrl); - if (moduleId != null && !this.isFileUrl(downloadUrl)) { - webView.evaluateJavascript("document.querySelector(" + - "\"#download-form input[name=_token]\").value", - result -> new Thread("Androidacy popup workaround thread") { - @Override - public void run() { - if (androidacyWebAPI.consumedAction) return; - try { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("moduleId", moduleId); - jsonObject.put("token", AndroidacyRepoData - .getInstance().getToken()); - jsonObject.put("_token", result); - String realUrl = Http.doHttpPostRedirect(downloadUrl, - jsonObject.toString(), true); - if (downloadUrl.equals(realUrl)) { - Log.e(TAG, "Failed to resolve URL from " + - downloadUrl); - AndroidacyActivity.this.megaIntercept( - webView.getUrl(), downloadUrl); - return; - } - Log.i(TAG, "Got url: " + realUrl); - androidacyWebAPI.openNativeModuleDialogRaw(realUrl, - moduleId, "", androidacyWebAPI.canInstall()); - } catch (IOException | JSONException e) { - Log.e(TAG, "Failed redirect intercept", e); - } - } - }.start()); - return; - } else if (this.megaIntercept(webView.getUrl(), downloadUrl)) + if (this.megaIntercept(webView.getUrl(), downloadUrl)) { + // Block request as Androidacy doesn't allow duplicate requests return; + } else if (moduleId != null) { + // Download module + Log.i(TAG, "megaIntercept failure. Forcing onBackPress"); + this.onBackPressed(); + } } androidacyWebAPI.consumedAction = true; androidacyWebAPI.downloadMode = false; } this.backOnResume = true; - Log.i(TAG, "Exiting WebView " + - AndroidacyUtil.hideToken(downloadUrl)); - for (String prefix : new String[]{ - "https://production-api.androidacy.com/magisk/download/", - "https://staging-api.androidacy.com/magisk/download/" - }) { + Log.i(TAG, "Exiting WebView " + AndroidacyUtil.hideToken(downloadUrl)); + for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { if (downloadUrl.startsWith(prefix)) { return; } @@ -345,8 +301,11 @@ public final class AndroidacyActivity extends FoxActivity { this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm"); if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel); HashMap headers = new HashMap<>(); - headers.put("Accept-Language", this.getResources() - .getConfiguration().locale.toLanguageTag()); + headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag()); + if (BuildConfig.DEBUG) { + headers.put("X-Debug", "true"); + Log.i(TAG, "Debug mode enabled for webview using URL: " + url + " with headers: " + headers); + } this.webView.loadUrl(url, headers); } @@ -372,14 +331,7 @@ public final class AndroidacyActivity extends FoxActivity { } private String moduleIdOfUrl(String url) { - for (String prefix : new String[]{ - "https://production-api.androidacy.com/magisk/download/", - "https://staging-api.androidacy.com/magisk/download/", - "https://production-api.androidacy.com/magisk/readme/", - "https://staging-api.androidacy.com/magisk/readme/", - "https://prodiuction-api.androidacy.com/magisk/info/", - "https://staging-api.androidacy.com/magisk/info/" - }) { // Make both staging and non staging act the same + for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/readme/", "https://staging-api.androidacy.com/magisk/readme/", "https://prodiuction-api.androidacy.com/magisk/info/", "https://staging-api.androidacy.com/magisk/info/"}) { // Make both staging and non staging act the same int i = url.indexOf('?', prefix.length()); if (i == -1) i = url.length(); if (url.startsWith(prefix)) return url.substring(prefix.length(), i); @@ -400,20 +352,14 @@ public final class AndroidacyActivity extends FoxActivity { private boolean isFileUrl(String url) { if (url == null) return false; - for (String prefix : new String[]{ - "https://production-api.androidacy.com/magisk/file/", - "https://staging-api.androidacy.com/magisk/file/" - }) { // Make both staging and non staging act the same + for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same if (url.startsWith(prefix)) return true; } return false; } private boolean isDownloadUrl(String url) { - for (String prefix : new String[]{ - "https://production-api.androidacy.com/magisk/download/", - "https://staging-api.androidacy.com/magisk/download/" - }) { // Make both staging and non staging act the same + for (String prefix : new String[]{"https://production-api.androidacy.com/magisk/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { // Make both staging and non staging act the same if (url.startsWith(prefix)) return true; } return false; @@ -422,19 +368,18 @@ public final class AndroidacyActivity extends FoxActivity { private boolean megaIntercept(String pageUrl, String fileUrl) { if (pageUrl == null || fileUrl == null) return false; if (this.isFileUrl(fileUrl)) { - Log.d(TAG, "megaIntercept(" + - AndroidacyUtil.hideToken(pageUrl) + ", " + - AndroidacyUtil.hideToken(fileUrl) + ")"); + Log.d(TAG, "megaIntercept(" + AndroidacyUtil.hideToken(pageUrl) + ", " + AndroidacyUtil.hideToken(fileUrl) + ")"); } else return false; final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; - String moduleId = this.moduleIdOfUrl(fileUrl); - if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl); + String moduleId = AndroidacyUtil.getModuleId(fileUrl); if (moduleId == null) { Log.d(TAG, "No module id?"); - return false; + // Re-open the page + this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis()); } - androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, - moduleId, "", androidacyWebAPI.canInstall()); + String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl); + String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl); + androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall()); return true; } @@ -446,21 +391,17 @@ public final class AndroidacyActivity extends FoxActivity { }); byte[] module; try { - module = Http.doHttpGet(url, (downloaded, total, done) -> - progressIndicator.setProgressCompat((downloaded * 100) / total, true)); + module = Http.doHttpGet(url, (downloaded, total, done) -> progressIndicator.setProgressCompat((downloaded * 100) / total, true)); try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) { fileOutputStream.write(module); } } finally { //noinspection UnusedAssignment module = null; - this.runOnUiThread(() -> - progressIndicator.setVisibility(View.INVISIBLE)); + this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE)); } this.backOnResume = true; this.downloadMode = false; - return FileProvider.getUriForFile(this, - this.getPackageName() + ".file-provider", - this.moduleFile); + return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile); } } diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index 961c170..8a0d8d2 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -6,6 +6,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.manager.ModuleInfo; @@ -45,7 +46,7 @@ public final class AndroidacyRepoData extends RepoData { private final String host; // Avoid spamming requests to Androidacy private long androidacyBlockade = 0; - private String token = this.cachedPreferences.getString("pref_androidacy_api_token", null); + public String token = this.cachedPreferences.getString("pref_androidacy_api_token", null); public AndroidacyRepoData(File cacheRoot, SharedPreferences cachedPreferences, boolean testMode) { super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot, cachedPreferences); @@ -83,7 +84,7 @@ public final class AndroidacyRepoData extends RepoData { Log.w(TAG, "Invalid token, resetting..."); // Remove saved preference SharedPreferences.Editor editor = this.cachedPreferences.edit(); - editor.remove("androidacy_api_token"); + editor.remove("pref_androidacy_api_token"); editor.apply(); return false; } @@ -117,8 +118,13 @@ public final class AndroidacyRepoData extends RepoData { this.token = this.cachedPreferences.getString("pref_androidacy_api_token", null); if (this.token != null && !this.isValidToken(this.token)) { this.token = null; + } else { + Log.i(TAG, "Using cached token"); } } else if (!this.isValidToken(this.token)) { + if (BuildConfig.DEBUG) { + throw new IllegalStateException("Invalid token: " + this.token); + } this.token = null; } } catch (IOException e) { @@ -130,9 +136,9 @@ public final class AndroidacyRepoData extends RepoData { } if (token == null) { try { - Log.i(TAG, "Refreshing token..."); + Log.i(TAG, "Requesting new token..."); // POST request to https://production-api.androidacy.com/auth/register - token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "foxmmm=true", false), StandardCharsets.UTF_8); + token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "{\"foxmmm\": \"true\"}", false), StandardCharsets.UTF_8); // Parse token try { JSONObject jsonObject = new JSONObject(token); @@ -151,7 +157,9 @@ public final class AndroidacyRepoData extends RepoData { return false; } // Save token to shared preference - MainApplication.getSharedPreferences().edit().putString("pref_androidacy_api_token", token).apply(); + SharedPreferences.Editor editor = this.cachedPreferences.edit(); + editor.putString("pref_androidacy_api_token", token); + editor.apply(); } catch (Exception e) { if (HttpException.shouldTimeout(e)) { Log.e(TAG, "We are being rate limited!", e); diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java index 7c25bdf..c146343 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java @@ -5,6 +5,8 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.fox2code.mmm.BuildConfig; + public class AndroidacyUtil { public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app"; @@ -50,4 +52,58 @@ public class AndroidacyUtil { "" + url.substring(i2); } } + + public static String getModuleId(String moduleUrl) { + // Get the &module= part + int i = moduleUrl.indexOf("&module="); + String moduleId; + // Match until next & or end + if (i != -1) { + int j = moduleUrl.indexOf('&', i + 1); + if (j == -1) { + moduleId = moduleUrl.substring(i + 8); + } else { + moduleId = moduleUrl.substring(i + 8, j); + } + // URL decode + moduleId = Uri.decode(moduleId); + // Strip non alphanumeric + moduleId = moduleId.replaceAll("[^a-zA-Z0-9]", ""); + return moduleId; + } + if (BuildConfig.DEBUG) { + throw new IllegalArgumentException("Invalid module url: " + moduleUrl); + } + return null; + } + + public static String getModuleTitle(String moduleUrl) { + // Get the &title= part + int i = moduleUrl.indexOf("&moduleTitle="); + // Match until next & or end + if (i != -1) { + int j = moduleUrl.indexOf('&', i + 1); + if (j == -1) { + return Uri.decode(moduleUrl.substring(i + 13)); + } else { + return Uri.decode(moduleUrl.substring(i + 13, j)); + } + } + return null; + } + + public static String getChecksumFromURL(String moduleUrl) { + // Get the &version= part + int i = moduleUrl.indexOf("&checksum="); + // Match until next & or end + if (i != -1) { + int j = moduleUrl.indexOf('&', i + 1); + if (j == -1) { + return moduleUrl.substring(i + 10); + } else { + return moduleUrl.substring(i + 10, j); + } + } + return null; + } } diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java index d72f581..d457947 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java @@ -62,9 +62,11 @@ public class AndroidacyWebAPI { this.downloadMode = false; } - void openNativeModuleDialogRaw(String moduleUrl, String installTitle, + void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle, String checksum, boolean canInstall) { - Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl)); + Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl) + + ", moduleId: " + moduleId + ", installTitle: " + installTitle + + ", checksum: " + checksum + ", canInstall: " + canInstall); this.downloadMode = false; RepoModule repoModule = AndroidacyRepoData .getInstance().moduleHashMap.get(installTitle); @@ -78,7 +80,8 @@ public class AndroidacyWebAPI { description = this.activity.getString(R.string.no_desc_found); } } else { - title = PropUtils.makeNameFromId(installTitle); + // URL Decode installTitle + title = installTitle; String checkSumType = Hashes.checkSumName(checksum); if (checkSumType == null) { description = "Checksum: " + (( @@ -249,6 +252,8 @@ public class AndroidacyWebAPI { this.forceQuitRaw("Androidacy didn't provided a valid checksum"); return; } + // moduleId is the module parameter in the url + String moduleId = AndroidacyUtil.getModuleId(moduleUrl); // Let's handle download mode ourself if not implemented if (this.effectiveCompatMode < 1) { if (!this.canInstall()) { @@ -256,7 +261,7 @@ public class AndroidacyWebAPI { this.activity.runOnUiThread(() -> this.activity.webView.loadUrl(moduleUrl)); } else { - this.openNativeModuleDialogRaw(moduleUrl, installTitle, checksum, true); + this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true); } } else { RepoModule repoModule = AndroidacyRepoData @@ -293,7 +298,9 @@ public class AndroidacyWebAPI { this.forceQuitRaw("Androidacy didn't provided a valid checksum"); return; } - this.openNativeModuleDialogRaw(moduleUrl, moduleId, checksum, this.canInstall()); + // Get moduleTitle from url +String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl); + this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall()); } /** diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java index 39b7703..472bd29 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -189,6 +189,7 @@ public final class RepoManager extends SyncManager { protected void scanInternal(@NonNull UpdateListener updateListener) { NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug(); + // First, check if we have internet connection noodleDebug.push("Downloading indexes"); this.modules.clear(); updateListener.update(0D); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java index e2614b9..e3eb620 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -1,10 +1,20 @@ package com.fox2code.mmm.repo; import android.util.Log; +import android.view.View; +import android.view.Window; +import android.widget.Toast; +import androidx.annotation.Nullable; + +import com.fox2code.mmm.MainActivity; +import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.utils.Files; import com.fox2code.mmm.utils.Http; +import com.fox2code.mmm.utils.HttpException; +import com.google.android.material.snackbar.Snackbar; +import org.jetbrains.annotations.Contract; import org.json.JSONObject; import java.io.IOException; @@ -40,6 +50,13 @@ public class RepoUpdater { return 0; } this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false); + // Ensure it's a valid json and response code is 200 + if (this.indexRaw.hashCode() == 0) { + this.indexRaw = null; + this.toUpdate = Collections.emptyList(); + this.toApply = this.repoData.moduleHashMap.values(); + return 0; + } this.toUpdate = this.repoData.populate(new JSONObject( new String(this.indexRaw, StandardCharsets.UTF_8))); // Since we reuse instances this should work diff --git a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java index b07dcc4..b671d0e 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -502,7 +502,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { String[] originalApiKeyRef = new String[]{ MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", "")}; // Create the pref_androidacy_repo_api_key text input with validation - EditTextPreference prefAndroidacyRepoApiKey = findPreference("pref_androidacy_repo_api_key"); + EditTextPreference prefAndroidacyRepoApiKey = findPreference("pref_androidacy_api_token"); assert prefAndroidacyRepoApiKey != null; prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> { editText.setSingleLine(); @@ -531,9 +531,36 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // If key is empty, just remove it and change the text of the snack bar if (apiKey.isEmpty()) { MainApplication.getSharedPreferences().edit().remove( - "pref_androidacy_repo_api_key").apply(); - new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), - R.string.api_key_removed, Snackbar.LENGTH_SHORT).show()); + "pref_androidacy_api_token").apply(); + new Handler(Looper.getMainLooper()).post(() -> { + Snackbar.make(requireView(), R.string.api_key_removed, Snackbar.LENGTH_SHORT).show(); + // Show dialog to restart app with ok button + new MaterialAlertDialogBuilder(this.requireContext()) + .setTitle(R.string.restart) + .setMessage(R.string.api_key_restart) + .setNeutralButton(android.R.string.ok, (dialog, which) -> { + // User clicked OK button + Intent mStartActivity = new Intent(requireContext(), MainActivity.class); + mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + int mPendingIntentId = 123456; + // If < 23, FLAG_IMMUTABLE is not available + PendingIntent mPendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, + mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, + mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); + } + AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Restarting app to save token preference: " + newValue); + } + System.exit(0); // Exit app process + }) + .show(); + }); } else { // If key < 64 chars, it's not valid if (apiKey.length() < 64) { @@ -548,6 +575,11 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid)); }); } else { + // If the key is the same as the original, just show a snack bar + if (apiKey.equals(originalApiKeyRef[0])) { + new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), R.string.api_key_unchanged, Snackbar.LENGTH_SHORT).show()); + return; + } boolean valid = false; try { valid = AndroidacyRepoData.getInstance().isValidToken(apiKey); @@ -557,9 +589,37 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { originalApiKeyRef[0] = apiKey; RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey); MainApplication.getSharedPreferences().edit().putString( - "pref_androidacy_repo_api_key", apiKey).apply(); - new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), - R.string.api_key_valid, Snackbar.LENGTH_SHORT).show()); + "pref_androidacy_api_token", apiKey).apply(); + // Snackbar with success and restart button + new Handler(Looper.getMainLooper()).post(() -> { + Snackbar.make(requireView(), R.string.api_key_valid, Snackbar.LENGTH_SHORT).show(); + // Show dialog to restart app with ok button + new MaterialAlertDialogBuilder(this.requireContext()) + .setTitle(R.string.restart) + .setMessage(R.string.api_key_restart) + .setNeutralButton(android.R.string.ok, (dialog, which) -> { + // User clicked OK button + Intent mStartActivity = new Intent(requireContext(), MainActivity.class); + mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + int mPendingIntentId = 123456; + // If < 23, FLAG_IMMUTABLE is not available + PendingIntent mPendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, + mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId, + mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); + } + AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Restarting app to save token preference: " + newValue); + } + System.exit(0); // Exit app process + }) + .show(); + }); } else { new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show(); 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 842efc8..35ee345 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -1,6 +1,8 @@ package com.fox2code.mmm.utils; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; @@ -13,16 +15,26 @@ import android.webkit.WebSettings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.fox2code.foxcompat.FoxActivity; +import com.fox2code.foxcompat.internal.FoxCompat; import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.MainApplication; +import com.fox2code.mmm.R; import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.repo.RepoManager; +import com.fox2code.mmm.settings.SettingsActivity; +import com.google.android.gms.net.CronetProviderInstaller; +import com.google.android.material.snackbar.Snackbar; +import com.google.net.cronet.okhttptransport.CronetInterceptor; + +import org.chromium.net.CronetEngine; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.net.Proxy; import java.net.UnknownHostException; @@ -42,13 +54,13 @@ import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.Dns; import okhttp3.HttpUrl; +import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; -import okhttp3.brotli.BrotliInterceptor; import okhttp3.dnsoverhttps.DnsOverHttps; import okio.BufferedSink; @@ -97,20 +109,10 @@ public class Http { httpclientBuilder.connectTimeout(15, TimeUnit.SECONDS); httpclientBuilder.writeTimeout(15, TimeUnit.SECONDS); httpclientBuilder.readTimeout(15, TimeUnit.SECONDS); - httpclientBuilder.addInterceptor(BrotliInterceptor.INSTANCE); httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy Dns dns = Dns.SYSTEM; try { - InetAddress[] cloudflareBootstrap = new InetAddress[]{ - InetAddress.getByName("162.159.36.1"), - InetAddress.getByName("162.159.46.1"), - InetAddress.getByName("1.1.1.1"), - InetAddress.getByName("1.0.0.1"), - InetAddress.getByName("162.159.132.53"), - InetAddress.getByName("2606:4700:4700::1111"), - InetAddress.getByName("2606:4700:4700::1001"), - InetAddress.getByName("2606:4700:4700::0064"), - InetAddress.getByName("2606:4700:4700::6400")}; + InetAddress[] cloudflareBootstrap = new InetAddress[]{InetAddress.getByName("162.159.36.1"), InetAddress.getByName("162.159.46.1"), InetAddress.getByName("1.1.1.1"), InetAddress.getByName("1.0.0.1"), InetAddress.getByName("162.159.132.53"), InetAddress.getByName("2606:4700:4700::1111"), InetAddress.getByName("2606:4700:4700::1001"), InetAddress.getByName("2606:4700:4700::0064"), InetAddress.getByName("2606:4700:4700::6400")}; dns = s -> { if ("cloudflare-dns.com".equals(s)) { return Arrays.asList(cloudflareBootstrap); @@ -119,21 +121,16 @@ public class Http { }; httpclientBuilder.dns(dns); httpclientBuilder.cookieJar(new CDNCookieJar()); - dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url( - Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))) - .bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build(); + dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))).bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build(); } catch (UnknownHostException | RuntimeException e) { Log.e(TAG, "Failed to init DoH", e); } httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); // User-Agent format was agreed on telegram if (hasWebView) { - androidacyUA = WebSettings.getDefaultUserAgent(mainApplication) - .replace("wv", "") + "FoxMmm/" + BuildConfig.VERSION_CODE; + androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE; } else { - androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + - " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + - " FoxMmm/" + BuildConfig.VERSION_CODE; + androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + " FoxMmm/" + BuildConfig.VERSION_CODE; } httpclientBuilder.addInterceptor(chain -> { Request.Builder request = chain.request().newBuilder(); @@ -141,8 +138,7 @@ public class Http { String host = chain.request().url().host(); if (host.endsWith(".androidacy.com")) { request.header("User-Agent", androidacyUA); - } else if (!(host.equals("github.com") || host.endsWith(".github.com") || - host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) { + } else if (!(host.equals("github.com") || host.endsWith(".github.com") || host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) { if (InstallerInitializer.peekMagiskPath() != null) { request.header("User-Agent", // Declare Magisk version to the server "Magisk/" + InstallerInitializer.peekMagiskVersion()); @@ -154,13 +150,22 @@ public class Http { } return chain.proceed(request.build()); }); + // Add cronet interceptor + // install cronet + try { + CronetProviderInstaller.installProvider(mainApplication); + } catch (Exception e) { + Log.e(TAG, "Failed to install cronet", e); + } + // init cronet + try { + CronetEngine engine = new CronetEngine.Builder(mainApplication).build(); + httpclientBuilder.addInterceptor(CronetInterceptor.newBuilder(engine).build()); + } catch (Exception e) { + Log.e(TAG, "Failed to init cronet", e); + } // Fallback DNS cache responses in case request fail but already succeeded once in the past - fallbackDNS = 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", "api.androidacy.com", - "production-api.androidacy.com"); + fallbackDNS = 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", "api.androidacy.com", "production-api.androidacy.com"); httpclientBuilder.cookieJar(cookieJar = new CDNCookieJar(cookieManager)); httpclientBuilder.dns(Dns.SYSTEM); httpClient = followRedirects(httpclientBuilder, true).build(); @@ -212,7 +217,7 @@ public class Http { public static boolean needCaptchaAndroidacy() { return needCaptchaAndroidacyHost != null; } - + public static String needCaptchaAndroidacyHost() { return needCaptchaAndroidacyHost; } @@ -221,15 +226,20 @@ public class Http { needCaptchaAndroidacyHost = null; } + @SuppressLint("RestrictedApi") @SuppressWarnings("resource") public static byte[] doHttpGet(String url, boolean allowCache) throws IOException { checkNeedBlockAndroidacyRequest(url); - Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()) - .newCall(new Request.Builder().url(url).get().build()).execute(); + Response response = + (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute(); // 200/204 == success, 304 == cache valid - if (response.code() != 200 && response.code() != 204 && - (response.code() != 304 || !allowCache)) { + if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { checkNeedCaptchaAndroidacy(url, response.code()); + // If it's a 401, and an androidacy link, it's probably an invalid token + MainApplication mainApplication = MainApplication.getINSTANCE(); + if (response.code() == 401 && AndroidacyUtil.isAndroidacyLink(url)) { + throw new HttpException("Androidacy token is invalid", 401); + } throw new HttpException(response.code()); } ResponseBody responseBody = response.body(); @@ -257,8 +267,7 @@ public class Http { return response.request().url().uri().toString(); } // 200/204 == success, 304 == cache valid - if (response.code() != 200 && response.code() != 204 && - (response.code() != 304 || !allowCache)) { + if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { checkNeedCaptchaAndroidacy(url, response.code()); throw new HttpException(response.code()); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6be519d..8b44069 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -181,4 +181,6 @@ Could not validate token for Androidacy. Please try again later. Unable to contact Androidacy server. Check your connection and try again. Androidacy update blocked by Captcha + API key has been changed. Restart the app to apply changes. + The API key you input is the same as the one already in use. diff --git a/app/src/main/res/xml/repo_preferences.xml b/app/src/main/res/xml/repo_preferences.xml index 8d3931f..e48698a 100644 --- a/app/src/main/res/xml/repo_preferences.xml +++ b/app/src/main/res/xml/repo_preferences.xml @@ -42,7 +42,7 @@ app:singleLineTitle="false" /> + android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" /> + \ No newline at end of file diff --git a/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java b/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java index 8cbb72e..185904a 100644 --- a/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java +++ b/app/src/sentry/java/com/fox2code/mmm/sentry/SentryMain.java @@ -29,6 +29,10 @@ public class SentryMain { public static final boolean IS_SENTRY_INSTALLED = true; private static final String TAG = "SentryMain"; + /** + * Initialize Sentry + * Sentry is used for crash reporting and performance monitoring. The SDK is explcitly configured not to send PII, and server side scrubbing of sensitive data is enabled (which also removes IP addresses) + */ public static void initialize(final MainApplication mainApplication) { SentryAndroid.init(mainApplication, options -> { // If crash reporting is disabled, stop here. @@ -39,11 +43,9 @@ public class SentryMain { // Sentry sends ABSOLUTELY NO Personally Identifiable Information (PII) by default. // Already set to false by default, just set it again to make peoples feel safer. options.setSendDefaultPii(false); - // It just tell if sentry should ping the sentry dsn to tell the app is running. - // This is not needed at all for crash reporting purposes, so disable it. - options.setEnableAutoSessionTracking(false); + // It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling. + options.setEnableAutoSessionTracking(true); // A screenshot of the app itself is only sent if the app crashes, and it only shows the last activity - // In addition, sentry is configured with a trusted third party other than sentry.io, and only trusted people have access to the sentry instance // Add a callback that will be used before the event is sent to Sentry. // With this callback, you can modify the event or, when returning null, also discard the event. options.setBeforeSend((event, hint) -> { diff --git a/build.gradle b/build.gradle index ed357b8..0f21606 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,10 @@ buildscript { project.ext.latestAboutLibsRelease = "10.5.0" project.ext.sentryConfigFile = new File(rootDir, "sentry.properties").getAbsoluteFile() project.ext.hasSentryConfig = sentryConfigFile.exists() + project.ext.sentryCli = [ + logLevel: "debug", + flavorAware: false + ] dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}" diff --git a/gradle.properties b/gradle.properties index 5f80030..5e3f284 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxPermSize=512m -XX:ReservedCodeCacheSize=512m +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:ReservedCodeCacheSize=512m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects