Loads of work

- General refactoring
- Significant speed improvements using cronet (currently depends on gms and will fallback without)
- Fix androidacy downloads
- More i probably forgot

Androidacy tokens and esp custom ones need more work but this is a good start

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/230/head
androidacy-user 2 years ago
parent 05a29b9a81
commit b6077f2256

@ -1,6 +1,6 @@
plugins { plugins {
// Gradle doesn't allow conditionally enabling/disabling 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.android.application'
id 'com.mikepenz.aboutlibraries.plugin' id 'com.mikepenz.aboutlibraries.plugin'
} }
@ -8,13 +8,14 @@ plugins {
android { android {
namespace "com.fox2code.mmm" namespace "com.fox2code.mmm"
compileSdk 33 compileSdk 33
buildToolsVersion '30.0.3'
defaultConfig { defaultConfig {
applicationId "com.fox2code.mmm" applicationId "com.fox2code.mmm"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 33
versionCode 59 versionCode 60
versionName "0.6.7" versionName "0.6.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -28,9 +29,9 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
debuggable true debuggable true
// ONLY FOR TESTING SENTRY // ONLY FOR TESTING SENTRY
// minifyEnabled true minifyEnabled true
// shrinkResources true shrinkResources true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
@ -137,7 +138,7 @@ sentry {
// as Gradle will resolve it to the latest version. // as Gradle will resolve it to the latest version.
// //
// Defaults to the latest published sentry 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 'androidx.work:work-runtime:2.7.1'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp-brotli: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.topjohnwu.libsu:io:5.0.1'
implementation 'com.github.Fox2Code:RosettaX:1.0.9' implementation 'com.github.Fox2Code:RosettaX:1.0.9'
implementation 'com.github.Fox2Code:AndroidANSI:1.0.1' implementation 'com.github.Fox2Code:AndroidANSI:1.0.1'
if (hasSentryConfig) { if (hasSentryConfig) {
// Error reporting // Error reporting
defaultImplementation 'io.sentry:sentry-android:6.5.0' defaultImplementation 'io.sentry:sentry-android:6.8.0'
defaultImplementation 'io.sentry:sentry-android-fragment:6.5.0' defaultImplementation 'io.sentry:sentry-android-fragment:6.8.0'
defaultImplementation 'io.sentry:sentry-android-okhttp:6.5.0' defaultImplementation 'io.sentry:sentry-android-okhttp:6.8.0'
defaultImplementation 'io.sentry:sentry-android-core:6.5.0' defaultImplementation 'io.sentry:sentry-android-core:6.8.0'
defaultImplementation 'io.sentry:sentry-android-ndk:6.5.0' defaultImplementation 'io.sentry:sentry-android-ndk:6.8.0'
} }
// Markdown // Markdown
@ -193,13 +195,13 @@ dependencies {
implementation "io.noties.markwon:html:4.6.2" implementation "io.noties.markwon:html:4.6.2"
implementation "io.noties.markwon:image:4.6.2" implementation "io.noties.markwon:image:4.6.2"
implementation "io.noties.markwon:syntax-highlight: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" annotationProcessor "io.noties:prism4j-bundler:2.0.0"
implementation "com.caverock:androidsvg:1.4" implementation "com.caverock:androidsvg:1.4"
// Test // Test
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
} }
if (hasSentryConfig) { if (hasSentryConfig) {

@ -185,4 +185,4 @@
int getSafeInsetRight(); int getSafeInsetRight();
int getSafeInsetTop(); int getSafeInsetTop();
android.graphics.Insets getWaterfallInsets(); android.graphics.Insets getWaterfallInsets();
} }

@ -1,4 +1,4 @@
#!/sbin/sh # shellcheck shell=ash
################# #################
# Initialization # Initialization
@ -20,24 +20,24 @@ require_new_magisk() {
# Load util_functions.sh # Load util_functions.sh
######################### #########################
OUTFD=$2 export OUTFD=$2
ZIPFILE=$3 export ZIPFILE=$3
mount /data 2>/dev/null mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh . /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 # Add grep_get_prop implementation if missing
if ! type grep_get_prop &>/dev/null; then if ! type grep_get_prop &>/dev/null; then
grep_get_prop() { grep_get_prop() {
local result=$(grep_prop $@) local result=$(grep_prop "$@")
if [ -z "$result" ]; then if [ -z "$result" ]; then
# Fallback to getprop # Fallback to getprop
getprop "$1" getprop "$1"
else else
echo $result echo "$result"
fi fi
} }
fi fi
@ -55,7 +55,7 @@ settings() {
fi 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 # New Magisk have complete installation logic within util_functions.sh
install_module install_module
exit 0 exit 0
@ -75,9 +75,10 @@ is_legacy_script() {
print_modname() { print_modname() {
local authlen len namelen pounds local authlen len namelen pounds
namelen=`echo -n $MODNAME | wc -c` # shellcheck disable=SC2006
authlen=$((`echo -n $MODAUTH | wc -c` + 3)) namelen=`echo -n "$MODNAME" | wc -c`
[ $namelen -gt $authlen ] && len=$namelen || len=$authlen authlen=$(($(echo -n "$MODAUTH" | wc -c) + 3))
[ "$namelen" -gt $authlen ] && len=$namelen || len=$authlen
len=$((len + 2)) len=$((len + 2))
pounds=$(printf "%${len}s" | tr ' ' '*') pounds=$(printf "%${len}s" | tr ' ' '*')
ui_print "$pounds" ui_print "$pounds"
@ -93,7 +94,7 @@ print_modname() {
abort() { abort() {
ui_print "$1" ui_print "$1"
$BOOTMODE || recovery_cleanup $BOOTMODE || recovery_cleanup
[ -n $MODPATH ] && rm -rf $MODPATH [ -n "$MODPATH" ] && rm -rf "$MODPATH"
rm -rf $TMPDIR rm -rf $TMPDIR
exit 1 exit 1
} }
@ -101,7 +102,7 @@ abort() {
rm -rf $TMPDIR 2>/dev/null rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR mkdir -p $TMPDIR
chcon u:object_r:system_file:s0 $TMPDIR || true chcon u:object_r:system_file:s0 $TMPDIR || true
cd $TMPDIR cd $TMPDIR || exit
# Preperation for flashable zips # Preperation for flashable zips
setup_flashable setup_flashable
@ -128,14 +129,15 @@ unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
MODDIRNAME=modules MODDIRNAME=modules
$BOOTMODE && MODDIRNAME=modules_update $BOOTMODE && MODDIRNAME=modules_update
MODULEROOT=$NVBASE/$MODDIRNAME MODULEROOT=$NVBASE/$MODDIRNAME
MODID=`grep_prop id $TMPDIR/module.prop` MODID=$(grep_prop id $TMPDIR/module.prop)
MODNAME=`grep_prop name $TMPDIR/module.prop` MODNAME=$(grep_prop name $TMPDIR/module.prop)
MODAUTH=`grep_prop author $TMPDIR/module.prop` MODAUTH=$(grep_prop author $TMPDIR/module.prop)
MODPATH=$MODULEROOT/$MODID MODPATH=$MODULEROOT/$MODID
# Create mod paths # Create mod paths
# shellcheck disable=SC2086
rm -rf $MODPATH 2>/dev/null rm -rf $MODPATH 2>/dev/null
mkdir -p $MODPATH mkdir -p "$MODPATH"
########## ##########
# Install # Install
@ -152,22 +154,22 @@ if is_legacy_script; then
on_install on_install
# Custom uninstaller # 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 # Skip mount
$SKIPMOUNT && touch $MODPATH/skip_mount $SKIPMOUNT && touch "$MODPATH"/skip_mount
# prop file # prop file
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop $PROPFILE && cp -af $TMPDIR/system.prop "$MODPATH"/system.prop
# Module info # Module info
cp -af $TMPDIR/module.prop $MODPATH/module.prop cp -af $TMPDIR/module.prop "$MODPATH"/module.prop
# post-fs-data scripts # 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 # service scripts
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh $LATESTARTSERVICE && cp -af $TMPDIR/service.sh "$MODPATH"/service.sh
ui_print "- Setting permissions" ui_print "- Setting permissions"
set_permissions set_permissions
@ -218,46 +220,46 @@ elif [ -n "$MMM_MMT_REBORN" ]; then
else else
print_modname 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" 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 # Default permissions
set_perm_recursive $MODPATH 0 0 0755 0644 set_perm_recursive "$MODPATH" 0 0 0755 0644
fi fi
# Load customization script # Load customization script
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh [ -f "$MODPATH"/customize.sh ] && . "$MODPATH"/customize.sh
fi fi
# Handle replace folders # Handle replace folders
for TARGET in $REPLACE; do for TARGET in $REPLACE; do
ui_print "- Replace target: $TARGET" ui_print "- Replace target: $TARGET"
mktouch $MODPATH$TARGET/.replace mktouch "$MODPATH""$TARGET"/.replace
done done
if $BOOTMODE; then if $BOOTMODE; then
# Update info for Magisk Manager # Update info for Magisk Manager
mktouch $NVBASE/modules/$MODID/update mktouch $NVBASE/modules/"$MODID"/update
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null rm -rf $NVBASE/modules/"$MODID"/remove 2>/dev/null
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null rm -rf $NVBASE/modules/"$MODID"/disable 2>/dev/null
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop cp -af "$MODPATH"/module.prop $NVBASE/modules/"$MODID"/module.prop
fi fi
# Copy over custom sepolicy rules # Copy over custom sepolicy rules
if ! type copy_sepolicy_rules &>/dev/null; then 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" ui_print "- Installing custom sepolicy patch"
# Remove old recovery logs (which may be filling partition) to make room # Remove old recovery logs (which may be filling partition) to make room
rm -f $PERSISTDIR/cache/recovery/* rm -f $PERSISTDIR/cache/recovery/*
PERSISTMOD=$PERSISTDIR/magisk/$MODID PERSISTMOD=$PERSISTDIR/magisk/$MODID
mkdir -p $PERSISTMOD mkdir -p "$PERSISTMOD"
cp -af $MODPATH/sepolicy.rule $PERSISTMOD/sepolicy.rule || abort "! Insufficient partition size" cp -af "$MODPATH"/sepolicy.rule "$PERSISTMOD"/sepolicy.rule || abort "! Insufficient partition size"
fi fi
else else
if [ -f $MODPATH/sepolicy.rule ]; then if [ -f "$MODPATH"/sepolicy.rule ]; then
ui_print "- Installing custom sepolicy rules" ui_print "- Installing custom sepolicy rules"
copy_sepolicy_rules copy_sepolicy_rules
fi fi
@ -265,9 +267,9 @@ fi
# Remove stuff that doesn't belong to modules and clean up any empty directories # Remove stuff that doesn't belong to modules and clean up any empty directories
rm -rf \ rm -rf \
$MODPATH/system/placeholder $MODPATH/customize.sh \ "$MODPATH"/system/placeholder "$MODPATH"/customize.sh \
$MODPATH/README.md $MODPATH/.git* 2>/dev/null "$MODPATH"/README.md "$MODPATH"/.git* 2>/dev/null
rmdir -p $MODPATH rmdir -p "$MODPATH"
############# #############
# Finalizing # Finalizing

@ -2,6 +2,7 @@ package com.fox2code.mmm;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
@ -65,6 +66,8 @@ public class MainApplication extends FoxApplication
private static String relPackageName = BuildConfig.APPLICATION_ID; private static String relPackageName = BuildConfig.APPLICATION_ID;
private static MainApplication INSTANCE; private static MainApplication INSTANCE;
private static boolean firstBoot; private static boolean firstBoot;
// Provides the Context for the base application
public Context FoxApplication = this;
static { static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create() Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create()

@ -74,8 +74,7 @@ public final class AndroidacyActivity extends FoxActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = this.getIntent(); Intent intent = this.getIntent();
Uri uri; Uri uri;
if (!MainApplication.checkSecret(intent) || if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) {
(uri = intent.getData()) == null) {
Log.w(TAG, "Impersonation detected"); Log.w(TAG, "Impersonation detected");
this.forceBackPressed(); this.forceBackPressed();
return; return;
@ -99,8 +98,14 @@ public final class AndroidacyActivity extends FoxActivity {
url = url + '?' + AndroidacyUtil.REFERRER; url = url + '?' + AndroidacyUtil.REFERRER;
} }
} }
boolean allowInstall = intent.getBooleanExtra( // Add token to url if not present
Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false); 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 title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE);
String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG); String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG);
int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0); 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); String configPkg = IntentHelper.getPackageOfConfig(config);
try { try {
XHooks.checkConfigTargetExists(this, configPkg, config); XHooks.checkConfigTargetExists(this, configPkg, config);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
menu -> { IntentHelper.openConfig(this, config);
IntentHelper.openConfig(this, config); return true;
return true; });
});
} catch (PackageManager.NameNotFoundException ignored) { } catch (PackageManager.NameNotFoundException ignored) {
} }
} }
@ -142,8 +146,7 @@ public final class AndroidacyActivity extends FoxActivity {
webSettings.setAllowContentAccess(false); webSettings.setAllowContentAccess(false);
// Attempt at fixing CloudFlare captcha. // Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) { if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) {
WebSettingsCompat.setRequestedWithHeaderMode( WebSettingsCompat.setRequestedWithHeaderMode(webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
} }
// If API level is .= 33, allow setAlgorithmicDarkeningAllowed // If API level is .= 33, allow setAlgorithmicDarkeningAllowed
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
@ -153,23 +156,19 @@ public final class AndroidacyActivity extends FoxActivity {
} }
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme
webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON);
WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// If api level is < 32, use force dark // If api level is < 32, use force dark
WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
} }
} }
this.webView.setWebViewClient(new WebViewClientCompat() { this.webView.setWebViewClient(new WebViewClientCompat() {
private String pageUrl; private String pageUrl;
@Override @Override
public boolean shouldOverrideUrlLoading( public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) {
@NonNull WebView view, @NonNull WebResourceRequest request) {
// Don't open non Androidacy urls inside WebView // Don't open non Androidacy urls inside WebView
if (request.isForMainFrame() && if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
!AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
if (downloadMode || backOnResume) return true; if (downloadMode || backOnResume) return true;
Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail. Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail.
AndroidacyUtil.hideToken(request.getUrl().toString())); AndroidacyUtil.hideToken(request.getUrl().toString()));
@ -181,13 +180,10 @@ public final class AndroidacyActivity extends FoxActivity {
@Nullable @Nullable
@Override @Override
public WebResourceResponse shouldInterceptRequest( public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebView view, WebResourceRequest request) { if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) {
if (AndroidacyActivity.this.megaIntercept(
this.pageUrl, request.getUrl().toString())) {
// Block request as Androidacy doesn't allow duplicate requests // Block request as Androidacy doesn't allow duplicate requests
return new WebResourceResponse("text/plain", "UTF-8", return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0]));
new ByteArrayInputStream(new byte[0]));
} }
return null; return null;
} }
@ -205,15 +201,11 @@ public final class AndroidacyActivity extends FoxActivity {
} }
private void onReceivedError(String url, int errorCode) { private void onReceivedError(String url, int errorCode) {
if ((url.startsWith("https://production-api.androidacy.com/magisk/") || 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)) {
url.startsWith("https://staging-api.androidacy.com/magisk/") || Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show();
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); AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed);
} else if (url.equals(this.pageUrl)) { } else if (url.equals(this.pageUrl)) {
postOnUiThread(() -> postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE));
webViewNote.setVisibility(View.VISIBLE));
} }
} }
@ -223,8 +215,7 @@ public final class AndroidacyActivity extends FoxActivity {
} }
@Override @Override
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) {
@NonNull WebResourceErrorCompat error) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
this.onReceivedError(request.getUrl().toString(), error.getErrorCode()); this.onReceivedError(request.getUrl().toString(), error.getErrorCode());
} }
@ -232,12 +223,8 @@ public final class AndroidacyActivity extends FoxActivity {
}); });
this.webView.setWebChromeClient(new WebChromeClient() { this.webView.setWebChromeClient(new WebChromeClient() {
@Override @Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
FileChooserParams fileChooserParams) { FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data)));
FoxActivity.getFoxActivity(webView).startActivityForResult(
fileChooserParams.createIntent(), (code, data) ->
filePathCallback.onReceiveValue(
FileChooserParams.parseResult(code, data)));
return true; return true;
} }
@ -277,62 +264,31 @@ public final class AndroidacyActivity extends FoxActivity {
progressIndicator.setVisibility(View.INVISIBLE); progressIndicator.setVisibility(View.INVISIBLE);
} }
}); });
this.webView.setDownloadListener(( this.webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return; if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return;
if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) { if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) {
AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
if (androidacyWebAPI != null) { if (androidacyWebAPI != null) {
if (!androidacyWebAPI.downloadMode) { if (!androidacyWebAPI.downloadMode) {
// Native module popup may cause download after consumed action // Native module popup may cause download after consumed action
if (androidacyWebAPI.consumedAction) if (androidacyWebAPI.consumedAction) return;
return;
// Workaround Androidacy bug // Workaround Androidacy bug
final String moduleId = moduleIdOfUrl(downloadUrl); final String moduleId = moduleIdOfUrl(downloadUrl);
if (moduleId != null && !this.isFileUrl(downloadUrl)) { if (this.megaIntercept(webView.getUrl(), downloadUrl)) {
webView.evaluateJavascript("document.querySelector(" + // Block request as Androidacy doesn't allow duplicate requests
"\"#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))
return; return;
} else if (moduleId != null) {
// Download module
Log.i(TAG, "megaIntercept failure. Forcing onBackPress");
this.onBackPressed();
}
} }
androidacyWebAPI.consumedAction = true; androidacyWebAPI.consumedAction = true;
androidacyWebAPI.downloadMode = false; androidacyWebAPI.downloadMode = false;
} }
this.backOnResume = true; this.backOnResume = true;
Log.i(TAG, "Exiting WebView " + Log.i(TAG, "Exiting WebView " + AndroidacyUtil.hideToken(downloadUrl));
AndroidacyUtil.hideToken(downloadUrl)); for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) {
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) {
if (downloadUrl.startsWith(prefix)) { if (downloadUrl.startsWith(prefix)) {
return; return;
} }
@ -345,8 +301,11 @@ public final class AndroidacyActivity extends FoxActivity {
this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm"); this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm");
if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel); if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel);
HashMap<String, String> headers = new HashMap<>(); HashMap<String, String> headers = new HashMap<>();
headers.put("Accept-Language", this.getResources() headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag());
.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); this.webView.loadUrl(url, headers);
} }
@ -372,14 +331,7 @@ public final class AndroidacyActivity extends FoxActivity {
} }
private String moduleIdOfUrl(String url) { private String moduleIdOfUrl(String url) {
for (String prefix : new String[]{ 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
"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
int i = url.indexOf('?', prefix.length()); int i = url.indexOf('?', prefix.length());
if (i == -1) i = url.length(); if (i == -1) i = url.length();
if (url.startsWith(prefix)) return url.substring(prefix.length(), i); 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) { private boolean isFileUrl(String url) {
if (url == null) return false; if (url == null) return false;
for (String prefix : new String[]{ 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
"https://production-api.androidacy.com/magisk/file/",
"https://staging-api.androidacy.com/magisk/file/"
}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true; if (url.startsWith(prefix)) return true;
} }
return false; return false;
} }
private boolean isDownloadUrl(String url) { private boolean isDownloadUrl(String url) {
for (String prefix : new String[]{ 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
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true; if (url.startsWith(prefix)) return true;
} }
return false; return false;
@ -422,19 +368,18 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean megaIntercept(String pageUrl, String fileUrl) { private boolean megaIntercept(String pageUrl, String fileUrl) {
if (pageUrl == null || fileUrl == null) return false; if (pageUrl == null || fileUrl == null) return false;
if (this.isFileUrl(fileUrl)) { if (this.isFileUrl(fileUrl)) {
Log.d(TAG, "megaIntercept(" + Log.d(TAG, "megaIntercept(" + AndroidacyUtil.hideToken(pageUrl) + ", " + AndroidacyUtil.hideToken(fileUrl) + ")");
AndroidacyUtil.hideToken(pageUrl) + ", " +
AndroidacyUtil.hideToken(fileUrl) + ")");
} else return false; } else return false;
final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
String moduleId = this.moduleIdOfUrl(fileUrl); String moduleId = AndroidacyUtil.getModuleId(fileUrl);
if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl);
if (moduleId == null) { if (moduleId == null) {
Log.d(TAG, "No module id?"); Log.d(TAG, "No module id?");
return false; // Re-open the page
this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis());
} }
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl);
moduleId, "", androidacyWebAPI.canInstall()); String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl);
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall());
return true; return true;
} }
@ -446,21 +391,17 @@ public final class AndroidacyActivity extends FoxActivity {
}); });
byte[] module; byte[] module;
try { try {
module = Http.doHttpGet(url, (downloaded, total, done) -> module = Http.doHttpGet(url, (downloaded, total, done) -> progressIndicator.setProgressCompat((downloaded * 100) / total, true));
progressIndicator.setProgressCompat((downloaded * 100) / total, true));
try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) { try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) {
fileOutputStream.write(module); fileOutputStream.write(module);
} }
} finally { } finally {
//noinspection UnusedAssignment //noinspection UnusedAssignment
module = null; module = null;
this.runOnUiThread(() -> this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE));
progressIndicator.setVisibility(View.INVISIBLE));
} }
this.backOnResume = true; this.backOnResume = true;
this.downloadMode = false; this.downloadMode = false;
return FileProvider.getUriForFile(this, return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile);
this.getPackageName() + ".file-provider",
this.moduleFile);
} }
} }

@ -6,6 +6,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R; import com.fox2code.mmm.R;
import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleInfo;
@ -45,7 +46,7 @@ public final class AndroidacyRepoData extends RepoData {
private final String host; private final String host;
// Avoid spamming requests to Androidacy // Avoid spamming requests to Androidacy
private long androidacyBlockade = 0; 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) { public AndroidacyRepoData(File cacheRoot, SharedPreferences cachedPreferences, boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot, cachedPreferences); 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..."); Log.w(TAG, "Invalid token, resetting...");
// Remove saved preference // Remove saved preference
SharedPreferences.Editor editor = this.cachedPreferences.edit(); SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.remove("androidacy_api_token"); editor.remove("pref_androidacy_api_token");
editor.apply(); editor.apply();
return false; return false;
} }
@ -117,8 +118,13 @@ public final class AndroidacyRepoData extends RepoData {
this.token = this.cachedPreferences.getString("pref_androidacy_api_token", null); this.token = this.cachedPreferences.getString("pref_androidacy_api_token", null);
if (this.token != null && !this.isValidToken(this.token)) { if (this.token != null && !this.isValidToken(this.token)) {
this.token = null; this.token = null;
} else {
Log.i(TAG, "Using cached token");
} }
} else if (!this.isValidToken(this.token)) { } else if (!this.isValidToken(this.token)) {
if (BuildConfig.DEBUG) {
throw new IllegalStateException("Invalid token: " + this.token);
}
this.token = null; this.token = null;
} }
} catch (IOException e) { } catch (IOException e) {
@ -130,9 +136,9 @@ public final class AndroidacyRepoData extends RepoData {
} }
if (token == null) { if (token == null) {
try { try {
Log.i(TAG, "Refreshing token..."); Log.i(TAG, "Requesting new token...");
// POST request to https://production-api.androidacy.com/auth/register // 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 // Parse token
try { try {
JSONObject jsonObject = new JSONObject(token); JSONObject jsonObject = new JSONObject(token);
@ -151,7 +157,9 @@ public final class AndroidacyRepoData extends RepoData {
return false; return false;
} }
// Save token to shared preference // 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) { } catch (Exception e) {
if (HttpException.shouldTimeout(e)) { if (HttpException.shouldTimeout(e)) {
Log.e(TAG, "We are being rate limited!", e); Log.e(TAG, "We are being rate limited!", e);

@ -5,6 +5,8 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.fox2code.mmm.BuildConfig;
public class AndroidacyUtil { public class AndroidacyUtil {
public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app"; public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app";
@ -50,4 +52,58 @@ public class AndroidacyUtil {
"<token>" + url.substring(i2); "<token>" + 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;
}
} }

@ -62,9 +62,11 @@ public class AndroidacyWebAPI {
this.downloadMode = false; this.downloadMode = false;
} }
void openNativeModuleDialogRaw(String moduleUrl, String installTitle, void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle,
String checksum, boolean canInstall) { 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; this.downloadMode = false;
RepoModule repoModule = AndroidacyRepoData RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle); .getInstance().moduleHashMap.get(installTitle);
@ -78,7 +80,8 @@ public class AndroidacyWebAPI {
description = this.activity.getString(R.string.no_desc_found); description = this.activity.getString(R.string.no_desc_found);
} }
} else { } else {
title = PropUtils.makeNameFromId(installTitle); // URL Decode installTitle
title = installTitle;
String checkSumType = Hashes.checkSumName(checksum); String checkSumType = Hashes.checkSumName(checksum);
if (checkSumType == null) { if (checkSumType == null) {
description = "Checksum: " + (( description = "Checksum: " + ((
@ -249,6 +252,8 @@ public class AndroidacyWebAPI {
this.forceQuitRaw("Androidacy didn't provided a valid checksum"); this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return; return;
} }
// moduleId is the module parameter in the url
String moduleId = AndroidacyUtil.getModuleId(moduleUrl);
// Let's handle download mode ourself if not implemented // Let's handle download mode ourself if not implemented
if (this.effectiveCompatMode < 1) { if (this.effectiveCompatMode < 1) {
if (!this.canInstall()) { if (!this.canInstall()) {
@ -256,7 +261,7 @@ public class AndroidacyWebAPI {
this.activity.runOnUiThread(() -> this.activity.runOnUiThread(() ->
this.activity.webView.loadUrl(moduleUrl)); this.activity.webView.loadUrl(moduleUrl));
} else { } else {
this.openNativeModuleDialogRaw(moduleUrl, installTitle, checksum, true); this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
} }
} else { } else {
RepoModule repoModule = AndroidacyRepoData RepoModule repoModule = AndroidacyRepoData
@ -293,7 +298,9 @@ public class AndroidacyWebAPI {
this.forceQuitRaw("Androidacy didn't provided a valid checksum"); this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return; 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());
} }
/** /**

@ -189,6 +189,7 @@ public final class RepoManager extends SyncManager {
protected void scanInternal(@NonNull UpdateListener updateListener) { protected void scanInternal(@NonNull UpdateListener updateListener) {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug(); NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
// First, check if we have internet connection
noodleDebug.push("Downloading indexes"); noodleDebug.push("Downloading indexes");
this.modules.clear(); this.modules.clear();
updateListener.update(0D); updateListener.update(0D);

@ -1,10 +1,20 @@
package com.fox2code.mmm.repo; package com.fox2code.mmm.repo;
import android.util.Log; 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.Files;
import com.fox2code.mmm.utils.Http; 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 org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
@ -40,6 +50,13 @@ public class RepoUpdater {
return 0; return 0;
} }
this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false); 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( this.toUpdate = this.repoData.populate(new JSONObject(
new String(this.indexRaw, StandardCharsets.UTF_8))); new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work // Since we reuse instances this should work

@ -502,7 +502,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
String[] originalApiKeyRef = new String[]{ String[] originalApiKeyRef = new String[]{
MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", "")}; MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", "")};
// Create the pref_androidacy_repo_api_key text input with validation // 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; assert prefAndroidacyRepoApiKey != null;
prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> { prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> {
editText.setSingleLine(); 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 key is empty, just remove it and change the text of the snack bar
if (apiKey.isEmpty()) { if (apiKey.isEmpty()) {
MainApplication.getSharedPreferences().edit().remove( MainApplication.getSharedPreferences().edit().remove(
"pref_androidacy_repo_api_key").apply(); "pref_androidacy_api_token").apply();
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), new Handler(Looper.getMainLooper()).post(() -> {
R.string.api_key_removed, Snackbar.LENGTH_SHORT).show()); 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 { } else {
// If key < 64 chars, it's not valid // If key < 64 chars, it's not valid
if (apiKey.length() < 64) { if (apiKey.length() < 64) {
@ -548,6 +575,11 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid)); prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid));
}); });
} else { } 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; boolean valid = false;
try { try {
valid = AndroidacyRepoData.getInstance().isValidToken(apiKey); valid = AndroidacyRepoData.getInstance().isValidToken(apiKey);
@ -557,9 +589,37 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
originalApiKeyRef[0] = apiKey; originalApiKeyRef[0] = apiKey;
RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey); RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey);
MainApplication.getSharedPreferences().edit().putString( MainApplication.getSharedPreferences().edit().putString(
"pref_androidacy_repo_api_key", apiKey).apply(); "pref_androidacy_api_token", apiKey).apply();
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), // Snackbar with success and restart button
R.string.api_key_valid, Snackbar.LENGTH_SHORT).show()); 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 { } else {
new Handler(Looper.getMainLooper()).post(() -> { new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show(); Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show();

@ -1,6 +1,8 @@
package com.fox2code.mmm.utils; package com.fox2code.mmm.utils;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -13,16 +15,26 @@ import android.webkit.WebSettings;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.internal.FoxCompat;
import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.repo.RepoManager; 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.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Proxy; import java.net.Proxy;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@ -42,13 +54,13 @@ import okhttp3.Cookie;
import okhttp3.CookieJar; import okhttp3.CookieJar;
import okhttp3.Dns; import okhttp3.Dns;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import okhttp3.brotli.BrotliInterceptor;
import okhttp3.dnsoverhttps.DnsOverHttps; import okhttp3.dnsoverhttps.DnsOverHttps;
import okio.BufferedSink; import okio.BufferedSink;
@ -97,20 +109,10 @@ public class Http {
httpclientBuilder.connectTimeout(15, TimeUnit.SECONDS); httpclientBuilder.connectTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.writeTimeout(15, TimeUnit.SECONDS); httpclientBuilder.writeTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.readTimeout(15, TimeUnit.SECONDS); httpclientBuilder.readTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.addInterceptor(BrotliInterceptor.INSTANCE);
httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy
Dns dns = Dns.SYSTEM; Dns dns = Dns.SYSTEM;
try { try {
InetAddress[] cloudflareBootstrap = new InetAddress[]{ 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.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 -> { dns = s -> {
if ("cloudflare-dns.com".equals(s)) { if ("cloudflare-dns.com".equals(s)) {
return Arrays.asList(cloudflareBootstrap); return Arrays.asList(cloudflareBootstrap);
@ -119,21 +121,16 @@ public class Http {
}; };
httpclientBuilder.dns(dns); httpclientBuilder.dns(dns);
httpclientBuilder.cookieJar(new CDNCookieJar()); httpclientBuilder.cookieJar(new CDNCookieJar());
dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url( dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))).bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query")))
.bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
} catch (UnknownHostException | RuntimeException e) { } catch (UnknownHostException | RuntimeException e) {
Log.e(TAG, "Failed to init DoH", e); Log.e(TAG, "Failed to init DoH", e);
} }
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
// User-Agent format was agreed on telegram // User-Agent format was agreed on telegram
if (hasWebView) { if (hasWebView) {
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication) androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE;
.replace("wv", "") + "FoxMmm/" + BuildConfig.VERSION_CODE;
} else { } else {
androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + 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;
" AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" +
" FoxMmm/" + BuildConfig.VERSION_CODE;
} }
httpclientBuilder.addInterceptor(chain -> { httpclientBuilder.addInterceptor(chain -> {
Request.Builder request = chain.request().newBuilder(); Request.Builder request = chain.request().newBuilder();
@ -141,8 +138,7 @@ public class Http {
String host = chain.request().url().host(); String host = chain.request().url().host();
if (host.endsWith(".androidacy.com")) { if (host.endsWith(".androidacy.com")) {
request.header("User-Agent", androidacyUA); request.header("User-Agent", androidacyUA);
} else if (!(host.equals("github.com") || host.endsWith(".github.com") || } else if (!(host.equals("github.com") || host.endsWith(".github.com") || host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
if (InstallerInitializer.peekMagiskPath() != null) { if (InstallerInitializer.peekMagiskPath() != null) {
request.header("User-Agent", // Declare Magisk version to the server request.header("User-Agent", // Declare Magisk version to the server
"Magisk/" + InstallerInitializer.peekMagiskVersion()); "Magisk/" + InstallerInitializer.peekMagiskVersion());
@ -154,13 +150,22 @@ public class Http {
} }
return chain.proceed(request.build()); 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 // 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", 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");
"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.cookieJar(cookieJar = new CDNCookieJar(cookieManager));
httpclientBuilder.dns(Dns.SYSTEM); httpclientBuilder.dns(Dns.SYSTEM);
httpClient = followRedirects(httpclientBuilder, true).build(); httpClient = followRedirects(httpclientBuilder, true).build();
@ -212,7 +217,7 @@ public class Http {
public static boolean needCaptchaAndroidacy() { public static boolean needCaptchaAndroidacy() {
return needCaptchaAndroidacyHost != null; return needCaptchaAndroidacyHost != null;
} }
public static String needCaptchaAndroidacyHost() { public static String needCaptchaAndroidacyHost() {
return needCaptchaAndroidacyHost; return needCaptchaAndroidacyHost;
} }
@ -221,15 +226,20 @@ public class Http {
needCaptchaAndroidacyHost = null; needCaptchaAndroidacyHost = null;
} }
@SuppressLint("RestrictedApi")
@SuppressWarnings("resource") @SuppressWarnings("resource")
public static byte[] doHttpGet(String url, boolean allowCache) throws IOException { public static byte[] doHttpGet(String url, boolean allowCache) throws IOException {
checkNeedBlockAndroidacyRequest(url); checkNeedBlockAndroidacyRequest(url);
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()) Response response =
.newCall(new Request.Builder().url(url).get().build()).execute(); (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute();
// 200/204 == success, 304 == cache valid // 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 && if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
(response.code() != 304 || !allowCache)) {
checkNeedCaptchaAndroidacy(url, response.code()); 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()); throw new HttpException(response.code());
} }
ResponseBody responseBody = response.body(); ResponseBody responseBody = response.body();
@ -257,8 +267,7 @@ public class Http {
return response.request().url().uri().toString(); return response.request().url().uri().toString();
} }
// 200/204 == success, 304 == cache valid // 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 && if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
(response.code() != 304 || !allowCache)) {
checkNeedCaptchaAndroidacy(url, response.code()); checkNeedCaptchaAndroidacy(url, response.code());
throw new HttpException(response.code()); throw new HttpException(response.code());
} }

@ -181,4 +181,6 @@
<string name="androidacy_failed_to_validate_token">Could not validate token for Androidacy. Please try again later.</string> <string name="androidacy_failed_to_validate_token">Could not validate token for Androidacy. Please try again later.</string>
<string name="androidacy_server_down">Unable to contact Androidacy server. Check your connection and try again.</string> <string name="androidacy_server_down">Unable to contact Androidacy server. Check your connection and try again.</string>
<string name="androidacy_need_captcha">Androidacy update blocked by Captcha</string> <string name="androidacy_need_captcha">Androidacy update blocked by Captcha</string>
<string name="api_key_restart">API key has been changed. Restart the app to apply changes.</string>
<string name="api_key_unchanged">The API key you input is the same as the one already in use.</string>
</resources> </resources>

@ -42,7 +42,7 @@
app:singleLineTitle="false" /> app:singleLineTitle="false" />
<!-- Allow user to set custom API key for Androidacy repo --> <!-- Allow user to set custom API key for Androidacy repo -->
<EditTextPreference <EditTextPreference
app:key="pref_androidacy_repo_api_key" app:key="pref_androidacy_api_token"
app:icon="@drawable/ic_baseline_vpn_key_24" app:icon="@drawable/ic_baseline_vpn_key_24"
app:title="@string/api_key" app:title="@string/api_key"
app:summary="@string/api_key_summary" app:summary="@string/api_key_summary"

@ -9,7 +9,7 @@
android:value="false" /> android:value="false" />
<meta-data <meta-data
android:name="io.sentry.dsn" android:name="io.sentry.dsn"
android:value="https://cdcdb0efca4a42a28df90e4b7f087347@sentry.androidacy.com/2" /> android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" />
<!-- Sane value, but feel free to lower it --> <!-- Sane value, but feel free to lower it -->
<meta-data <meta-data
android:name="io.sentry.traces.sample-rate" android:name="io.sentry.traces.sample-rate"
@ -30,5 +30,8 @@
<meta-data <meta-data
android:name="io.sentry.sendDefaultPii" android:name="io.sentry.sendDefaultPii"
android:value="false" /> android:value="false" />
<meta-data
android:name="io.sentry.traces.profiling.sample-rate"
android:value="0.5" />
</application> </application>
</manifest> </manifest>

@ -29,6 +29,10 @@ public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true; public static final boolean IS_SENTRY_INSTALLED = true;
private static final String TAG = "SentryMain"; 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) { public static void initialize(final MainApplication mainApplication) {
SentryAndroid.init(mainApplication, options -> { SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here. // If crash reporting is disabled, stop here.
@ -39,11 +43,9 @@ public class SentryMain {
// Sentry sends ABSOLUTELY NO Personally Identifiable Information (PII) by default. // 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. // Already set to false by default, just set it again to make peoples feel safer.
options.setSendDefaultPii(false); options.setSendDefaultPii(false);
// It just tell if sentry should ping the sentry dsn to tell the app is running. // It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
// This is not needed at all for crash reporting purposes, so disable it. options.setEnableAutoSessionTracking(true);
options.setEnableAutoSessionTracking(false);
// A screenshot of the app itself is only sent if the app crashes, and it only shows the last activity // 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. // 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. // With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> { options.setBeforeSend((event, hint) -> {

@ -8,6 +8,10 @@ buildscript {
project.ext.latestAboutLibsRelease = "10.5.0" project.ext.latestAboutLibsRelease = "10.5.0"
project.ext.sentryConfigFile = new File(rootDir, "sentry.properties").getAbsoluteFile() project.ext.sentryConfigFile = new File(rootDir, "sentry.properties").getAbsoluteFile()
project.ext.hasSentryConfig = sentryConfigFile.exists() project.ext.hasSentryConfig = sentryConfigFile.exists()
project.ext.sentryCli = [
logLevel: "debug",
flavorAware: false
]
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.android.tools.build:gradle:7.3.1'
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"

@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

Loading…
Cancel
Save