From 2171364db828657719b3dbbf4911b175f904e478 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Fri, 21 Oct 2022 20:22:29 -0400 Subject: [PATCH 01/19] Replace captcha webview with warning Signed-off-by: androidacy-user --- .../mmm/settings/SettingsActivity.java | 2 ++ .../java/com/fox2code/mmm/utils/Http.java | 35 +++++-------------- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/repo_preferences.xml | 5 +++ 4 files changed, 16 insertions(+), 27 deletions(-) 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 0da0947dd..11996f193 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -387,6 +387,8 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"}) public void onCreatePreferencesAndroidacy() { + // Bind the pref_show_captcha_webview to captchaWebview('https://production-api.androidacy.com/') + // Also require dev modeowCaptchaWebview.setVisible(false); Preference androidacyTestMode = Objects.requireNonNull(findPreference("pref_androidacy_test_mode")); if (!MainApplication.isDeveloper()) { androidacyTestMode.setVisible(false); 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 0a488b795..833d89fa5 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -1,24 +1,23 @@ package com.fox2code.mmm.utils; -import static com.fox2code.mmm.MainApplication.getInstance; - -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.system.ErrnoException; import android.system.Os; import android.util.Log; -import android.view.View; import android.webkit.CookieManager; import android.webkit.WebSettings; -import android.webkit.WebView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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; @@ -57,7 +56,6 @@ import okio.BufferedSink; public class Http { - private static final MainApplication mainApplication = getInstance(); private static final String TAG = "Http"; private static final OkHttpClient httpClient; private static final OkHttpClient httpClientDoH; @@ -177,25 +175,6 @@ public static OkHttpClient getHttpClientWithCache() { return doh ? httpClientWithCacheDoH : httpClientWithCache; } - @SuppressLint("SetJavaScriptEnabled") - public static void captchaWebview(String url) { - if (hasWebView) { - // Open the specified url in a webview - WebView webView = new WebView(mainApplication); - webView.getSettings().setJavaScriptEnabled(true); - webView.getSettings().setUserAgentString(androidacyUA); - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - // Open the url in the webview - webView.loadUrl(url); - // Show the webview - webView.setVisibility(View.VISIBLE); - } else { - // Throw an exception if the webview is not available - throw new IllegalStateException("Webview is not available"); - } - } - @SuppressWarnings("resource") public static byte[] doHttpGet(String url, boolean allowCache) throws IOException { if (!RepoManager.isAndroidacyRepoEnabled() && AndroidacyUtil.isAndroidacyLink(url)) { @@ -206,7 +185,8 @@ public static byte[] doHttpGet(String url, boolean allowCache) throws IOExceptio if (response.code() == 403 && AndroidacyUtil.isAndroidacyLink(url)) { // Open webview to solve captcha Log.e(TAG, "Received 403 error code, opening webview to solve captcha"); - captchaWebview(url); + // Show toast on main thread + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(MainApplication.getInstance(), R.string.androidacy_server_down, Toast.LENGTH_LONG).show()); } else if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { throw new IOException("Received error code: " + response.code()); } @@ -240,7 +220,8 @@ private static Object doHttpPostRaw(String url, String data, boolean allowCache, if (response.code() == 403 && AndroidacyUtil.isAndroidacyLink(url)) { // Open webview to solve captcha Log.e(TAG, "Received 403 error code, opening webview to solve captcha"); - captchaWebview(url); + // Show toast on main thread + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(MainApplication.getInstance(), R.string.androidacy_server_down, Toast.LENGTH_LONG).show()); } else if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) { throw new IOException("Received error code: " + response.code()); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 194914b7c..59925450f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -179,4 +179,5 @@ Could not retrieve token from Androidacy. Please try again later. Could not validate token for Androidacy. Please try again later. Unable to contact Androidacy server. Check your connection and try again. + Test the captcha webview implementation diff --git a/app/src/main/res/xml/repo_preferences.xml b/app/src/main/res/xml/repo_preferences.xml index 8cbb1a8f0..0e43d3d16 100644 --- a/app/src/main/res/xml/repo_preferences.xml +++ b/app/src/main/res/xml/repo_preferences.xml @@ -81,6 +81,11 @@ app:icon="@drawable/ic_baseline_upload_file_24" app:title="@string/submit_modules" app:singleLineTitle="false" /> + Date: Wed, 9 Nov 2022 09:13:00 -0500 Subject: [PATCH 02/19] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 383b714d3..56d7636e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Fox's Magisk Module Manager -## Important +
+ + Important news + I have health problems that made me work slow on everything. I don't like sharing my health problmes but it has been to much recently for me to keep it for myself. @@ -50,6 +53,8 @@ at least you won't be hurting peoples with mental/health issues by faking having I'll probably delete this section once my health would be gotten better, or at least good enough for me to not be stuck on my bed at least once a day because of pain. +
+ ## Community [![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2FFox2Code_Chat)](https://telegram.dog/Fox2Code_Chat) From c88de9cb123d03138a49585d8f3eddc2771b3bee Mon Sep 17 00:00:00 2001 From: Androidacy Service Account Date: Wed, 9 Nov 2022 09:15:42 -0500 Subject: [PATCH 03/19] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 56d7636e0..f8439ea4b 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,9 @@ and download and install the latest `.apk` on your device. ## Repositories Available -The app currently use these three repo as it's module sources, with it's benefits and drawback: +The app currently use these two repos as module sources, each with their own benefits and drawback: (Note: Each module repo can be disabled in the settings of the app) -(Note²: I do not own or monitor any of the repo, **download at your own risk**) +(Note²: I do not own or actively monitor any of the repos or modules, **download at your own risk**) #### [https://github.com/Magisk-Modules-Alt-Repo](https://github.com/Magisk-Modules-Alt-Repo) - Accepting new modules [here](https://github.com/Magisk-Modules-Alt-Repo/submission) @@ -117,6 +117,7 @@ Support: - Modules downloadable easily outside the app - Officially supported by Fox's mmm - Contains ads to help cover server costs +- Added features like module reviews, automatic VirusTotal scans, and more Support: From b6077f225608763772606d1a7d396af7366c5b95 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Fri, 25 Nov 2022 21:00:30 -0500 Subject: [PATCH 04/19] 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 --- app/build.gradle | 30 +-- app/proguard-rules.pro | 2 +- .../main/assets/module_installer_compat.sh | 80 ++++---- .../com/fox2code/mmm/MainApplication.java | 3 + .../mmm/androidacy/AndroidacyActivity.java | 171 ++++++------------ .../mmm/androidacy/AndroidacyRepoData.java | 18 +- .../mmm/androidacy/AndroidacyUtil.java | 56 ++++++ .../mmm/androidacy/AndroidacyWebAPI.java | 17 +- .../com/fox2code/mmm/repo/RepoManager.java | 1 + .../com/fox2code/mmm/repo/RepoUpdater.java | 17 ++ .../mmm/settings/SettingsActivity.java | 74 +++++++- .../java/com/fox2code/mmm/utils/Http.java | 79 ++++---- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/repo_preferences.xml | 2 +- app/src/sentry/AndroidManifest.xml | 5 +- .../com/fox2code/mmm/sentry/SentryMain.java | 10 +- build.gradle | 4 + gradle.properties | 2 +- 18 files changed, 345 insertions(+), 228 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dab0edcee..5ff7f8482 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 2a40c084d..0d82679d3 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 07ef0abfc..726912bd9 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 e70319ea4..5133a5148 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 @@ 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 6da67d605..7f4a1b5e5 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } } 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 boolean shouldOverrideUrlLoading( @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 void onPageFinished(WebView view, String url) { } 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 void onReceivedError(WebView view, int errorCode, String description, Str } @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 void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest r }); 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 void onProgressChanged(WebView view, int newProgress) { 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 void run() { 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 @@ protected void onResume() { } 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 @@ private String moduleIdOfUrl(String url) { 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 @@ private boolean isDownloadUrl(String url) { 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 @@ Uri downloadFileAsync(String url) throws IOException { }); 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 961c17003..8a0d8d2e7 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 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 boolean isValidToken(String token) throws IOException { 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 @@ protected boolean prepare() { 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 @@ protected boolean prepare() { } 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 @@ protected boolean prepare() { 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 7c25bdfa7..c14634396 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 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 static String hideToken(@NonNull String url) { "" + 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 d72f5814c..d457947a9 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 @@ void forceQuitRaw(String error) { 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 @@ void openNativeModuleDialogRaw(String moduleUrl, String installTitle, 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 void install(String moduleUrl, String installTitle, String checksum) { 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 void install(String moduleUrl, String installTitle, String checksum) { 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 void openNativeModuleDialog(String moduleUrl, String moduleId, String che 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 39b770332..472bd2954 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 RepoData addOrGet(String url, String fallBackName) { 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 e2614b9be..e3eb620b0 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 int fetchIndex() { 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 b07dcc453..b671d0eb3 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 void onCreatePreferencesAndroidacy() { 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 void onCreatePreferencesAndroidacy() { // 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 void onCreatePreferencesAndroidacy() { 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 void onCreatePreferencesAndroidacy() { 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 842efc82b..35ee345a4 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 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.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 @@ private static void checkNeedBlockAndroidacyRequest(String url) throws IOExcepti public static boolean needCaptchaAndroidacy() { return needCaptchaAndroidacyHost != null; } - + public static String needCaptchaAndroidacyHost() { return needCaptchaAndroidacyHost; } @@ -221,15 +226,20 @@ public static void markCaptchaAndroidacySolved() { 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 @@ private static Object doHttpPostRaw(String url, String data, boolean allowCache, 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 6be519d38..8b440697e 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 8d3931f1b..e48698a7b 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 8cbb72ee5..185904af0 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 static void initialize(final MainApplication mainApplication) { // 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 ed357b8f3..0f216062b 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 5f8003073..5e3f28469 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 From 7c934e9987c37592ce6fdceca37957fba34aed7d Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sat, 26 Nov 2022 11:48:06 -0500 Subject: [PATCH 05/19] Code cleanup and minor fixes Still need to address custom repo toggling not being saved Signed-off-by: androidacy-user --- app/proguard-rules.pro | 13 ++- .../com/fox2code/mmm/repo/CustomRepoData.java | 5 - .../java/com/fox2code/mmm/repo/RepoData.java | 25 +++-- .../java/com/fox2code/mmm/utils/Http.java | 102 +++--------------- build.gradle | 2 +- 5 files changed, 45 insertions(+), 102 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0d82679d3..6840bbe2f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -185,4 +185,15 @@ int getSafeInsetRight(); int getSafeInsetTop(); android.graphics.Insets getWaterfallInsets(); -} \ No newline at end of file +} + +# Silence some warnings +-dontwarn android.os.SystemProperties +-dontwarn android.view.ThreadedRenderer +-dontwarn cyanogenmod.providers.CMSettings$Secure +-dontwarn lineageos.providers.LineageSettings$System +-dontwarn lineageos.style.StyleInterface +-dontwarn me.weishu.reflection.Reflection +-dontwarn org.lsposed.hiddenapibypass.HiddenApiBypass +-dontwarn rikka.core.res.ResourcesCompatLayoutInflaterListener +-dontwarn rikka.core.util.ResourceUtils \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java index 94ae43434..31aca8fd8 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java @@ -30,11 +30,6 @@ public String getPreferenceId() { this.id : this.override; } - @Override - public boolean isLimited() { - return true; - } - public void quickPrePopulate() throws IOException, JSONException { JSONObject jsonObject = new JSONObject( new String(Http.doHttpGet(this.getUrl(), diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index ecd2567bc..70b53d978 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -27,7 +27,6 @@ import java.util.List; public class RepoData extends XRepo { - private static final String TAG = "RepoData"; private final Object populateLock = new Object(); public final String url; public final String id; @@ -54,7 +53,10 @@ protected RepoData(String url, File cacheRoot, SharedPreferences cachedPreferenc .getBoolean("pref_" + this.id + "_enabled", this.isEnabledByDefault()); this.defaultWebsite = "https://" + Uri.parse(url).getHost() + "/"; if (!this.cacheRoot.isDirectory()) { - this.cacheRoot.mkdirs(); + boolean mkdirs = this.cacheRoot.mkdirs(); + if (!mkdirs) { + throw new RuntimeException("Failed to create cache directory"); + } } else { if (this.metaDataCache.exists()) { this.lastUpdate = metaDataCache.lastModified(); @@ -70,7 +72,10 @@ protected RepoData(String url, File cacheRoot, SharedPreferences cachedPreferenc } } } catch (Exception e) { - this.metaDataCache.delete(); + boolean delete = this.metaDataCache.delete(); + if (!delete) { + throw new RuntimeException("Failed to delete invalid cache file"); + } } } } @@ -140,7 +145,10 @@ protected List populate(JSONObject jsonObject) throws JSONException while (moduleInfoIterator.hasNext()) { RepoModule repoModule = moduleInfoIterator.next(); if (!repoModule.processed) { - new File(this.cacheRoot, repoModule.id + ".prop").delete(); + boolean delete = new File(this.cacheRoot, repoModule.id + ".prop").delete(); + if (!delete) { + throw new RuntimeException("Failed to delete module metadata"); + } moduleInfoIterator.remove(); } else { repoModule.moduleInfo.verify(); @@ -179,7 +187,10 @@ public boolean tryLoadMetadata(RepoModule repoModule) { } return true; } catch (Exception ignored) { - file.delete(); + boolean delete = file.delete(); + if (!delete) { + throw new RuntimeException("Failed to delete invalid metadata file"); + } } } repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID; @@ -208,10 +219,6 @@ public String getUrl() { return this.url; } - public boolean isLimited() { - return false; - } - public String getPreferenceId() { return this.id; } 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 35ee345a4..39dc43c35 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -2,7 +2,6 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; @@ -15,17 +14,12 @@ 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; @@ -34,7 +28,6 @@ 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; @@ -54,7 +47,6 @@ import okhttp3.CookieJar; import okhttp3.Dns; import okhttp3.HttpUrl; -import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -70,10 +62,7 @@ public class Http { private static final OkHttpClient httpClientDoH; private static final OkHttpClient httpClientWithCache; private static final OkHttpClient httpClientWithCacheDoH; - private static final OkHttpClient httpClientNoRedirect; - private static final OkHttpClient httpClientNoRedirectDoH; private static final FallBackDNS fallbackDNS; - private static final CDNCookieJar cookieJar; private static final String androidacyUA; private static final boolean hasWebView; private static String needCaptchaAndroidacyHost; @@ -153,26 +142,33 @@ public class Http { // Add cronet interceptor // install cronet try { - CronetProviderInstaller.installProvider(mainApplication); + // Detect if cronet is installed + CronetProviderInstaller.installProvider(mainApplication); } catch (Exception e) { Log.e(TAG, "Failed to install cronet", e); } // init cronet try { - CronetEngine engine = new CronetEngine.Builder(mainApplication).build(); + CronetEngine.Builder builder = new CronetEngine.Builder(mainApplication); + builder.enableBrotli(true); + builder.enableHttp2(true); + builder.enableQuic(true); + CronetEngine engine = + builder.build(); httpclientBuilder.addInterceptor(CronetInterceptor.newBuilder(engine).build()); } catch (Exception e) { Log.e(TAG, "Failed to init cronet", e); + // Gracefully fallback to okhttp } // 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"); - httpclientBuilder.cookieJar(cookieJar = new CDNCookieJar(cookieManager)); + httpclientBuilder.cookieJar(new CDNCookieJar(cookieManager)); httpclientBuilder.dns(Dns.SYSTEM); httpClient = followRedirects(httpclientBuilder, true).build(); - httpClientNoRedirect = followRedirects(httpclientBuilder, false).build(); + followRedirects(httpclientBuilder, false).build(); httpclientBuilder.dns(fallbackDNS); httpClientDoH = followRedirects(httpclientBuilder, true).build(); - httpClientNoRedirectDoH = followRedirects(httpclientBuilder, false).build(); + followRedirects(httpclientBuilder, false).build(); httpclientBuilder.cache(new Cache(new File(mainApplication.getCacheDir(), "http_cache"), 16L * 1024L * 1024L)); // 16Mib of cache httpclientBuilder.dns(Dns.SYSTEM); httpClientWithCache = followRedirects(httpclientBuilder, true).build(); @@ -190,10 +186,6 @@ public static OkHttpClient getHttpClient() { return doh ? httpClientDoH : httpClient; } - public static OkHttpClient getHttpClientNoRedirect() { - return doh ? httpClientNoRedirectDoH : httpClientNoRedirect; - } - public static OkHttpClient getHttpClientWithCache() { return doh ? httpClientWithCacheDoH : httpClientWithCache; } @@ -236,7 +228,6 @@ public static byte[] doHttpGet(String url, boolean allowCache) throws IOExceptio 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); } @@ -252,18 +243,14 @@ public static byte[] doHttpGet(String url, boolean allowCache) throws IOExceptio } public static byte[] doHttpPost(String url, String data, boolean allowCache) throws IOException { - return (byte[]) doHttpPostRaw(url, data, allowCache, false); - } - - public static String doHttpPostRedirect(String url, String data, boolean allowCache) throws IOException { - return (String) doHttpPostRaw(url, data, allowCache, true); + return (byte[]) doHttpPostRaw(url, data, allowCache); } @SuppressWarnings("resource") - private static Object doHttpPostRaw(String url, String data, boolean allowCache, boolean isRedirect) throws IOException { + private static Object doHttpPostRaw(String url, String data, boolean allowCache) throws IOException { checkNeedBlockAndroidacyRequest(url); - Response response = (isRedirect ? getHttpClientNoRedirect() : allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute(); - if (isRedirect && response.isRedirect()) { + Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute(); + if (response.isRedirect()) { return response.request().url().uri().toString(); } // 200/204 == success, 304 == cache valid @@ -329,61 +316,15 @@ public static String getAndroidacyUA() { return androidacyUA; } - public static String getMagiskUA() { - return "Magisk/" + InstallerInitializer.peekMagiskVersion(); - } - public static void setDoh(boolean doh) { Log.d(TAG, "DoH: " + Http.doh + " -> " + doh); Http.doh = doh; } - public static String getAndroidacyCookies(String url) { - if (!AndroidacyUtil.isAndroidacyLink(url)) return ""; - return cookieJar.getAndroidacyCookies(url); - } - public static boolean hasWebView() { return hasWebView; } - /** - * Change URL to appropriate url and force Magisk link to use latest version. - */ - public static String updateLink(String string) { - if (string.startsWith("https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/")) { - String tmp = string.substring(48); - int start = tmp.lastIndexOf('@'), end = tmp.lastIndexOf('/'); - if ((end - 8) <= start) return string; // Skip if not a commit id - return "https://raw.githubusercontent.com/" + tmp.substring(0, start) + "/master" + string.substring(end); - } - if (string.startsWith("https://github.com/Magisk-Modules-Repo/")) { - int i = string.lastIndexOf("/archive/"); - if (i != -1 && string.indexOf('/', i + 9) == -1) - return string.substring(0, i + 9) + "master.zip"; - } - return string; - } - - /** - * Change GitHub user-content url to jsdelivr url - * (Unused but kept as a documentation) - */ - public static String cdnIfyLink(String string) { - if (string.startsWith("https://raw.githubusercontent.com/")) { - String[] tokens = string.substring(34).split("/", 4); - if (tokens.length != 4) return string; - return "https://cdn.jsdelivr.net/gh/" + tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3]; - } - if (string.startsWith("https://github.com/")) { - int i = string.lastIndexOf("/archive/"); - if (i == -1 || string.indexOf('/', i + 9) != -1) return string; // Not an archive link - String[] tokens = string.substring(19).split("/", 4); - return "https://cdn.jsdelivr.net/gh/" + tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3]; - } - return string; - } - public interface ProgressListener { void onUpdate(int downloaded, int total, boolean done); } @@ -471,17 +412,6 @@ public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List coo } } - String getAndroidacyCookies(String url) { - if (this.cookieManager != null) { - return this.cookieManager.getCookie(url); - } - StringBuilder stringBuilder = new StringBuilder(); - for (Cookie cookie : this.androidacyCookies) { - stringBuilder.append(cookie.toString()).append("; "); - } - stringBuilder.setLength(stringBuilder.length() - 2); - return stringBuilder.toString(); - } } /** diff --git a/build.gradle b/build.gradle index 0f216062b..5d064ef3e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { project.ext.hasSentryConfig = sentryConfigFile.exists() project.ext.sentryCli = [ logLevel: "debug", - flavorAware: false + flavorAware: true ] dependencies { classpath 'com.android.tools.build:gradle:7.3.1' From 55b2b5c040ccba91e160e57548a9e68b465da847 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sat, 26 Nov 2022 11:48:47 -0500 Subject: [PATCH 06/19] Fix weird proguard error Signed-off-by: androidacy-user --- app/proguard-rules.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6840bbe2f..3f7467fd7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -50,7 +50,7 @@ static void enableDebugLogging(boolean); } -assumevalues class androidx.loader.app.LoaderManagerImpl { - static boolean DEBUG return false; + static boolean DEBUG; } # This is just some proguard rules testes, might do a separate lib after From 37b19f01b66a116926aebc24f9246749c46235a9 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sat, 26 Nov 2022 21:33:20 -0500 Subject: [PATCH 07/19] Handle notification perm properly Plus other refactorings Signed-off-by: androidacy-user --- .../java/com/fox2code/mmm/MainActivity.java | 37 +++++++++++++------ .../com/fox2code/mmm/MainApplication.java | 5 +++ .../fox2code/mmm/repo/CustomRepoManager.java | 4 ++ .../mmm/settings/SettingsActivity.java | 9 ++++- app/src/main/res/values/strings.xml | 4 ++ 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 147169ade..3641747d8 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -1,6 +1,7 @@ package com.fox2code.mmm; import android.Manifest; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; @@ -20,6 +21,7 @@ import androidx.cardview.widget.CardView; import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -39,12 +41,10 @@ import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; import com.fox2code.mmm.utils.NoodleDebug; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; -import com.topjohnwu.superuser.Shell; import eightbitlab.com.blurview.BlurView; -import eightbitlab.com.blurview.RenderEffectBlur; -import eightbitlab.com.blurview.RenderScriptBlur; public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, @@ -87,10 +87,10 @@ protected void onCreate(Bundle savedInstanceState) { noodleDebugState = MainApplication.isDeveloper(); BackgroundUpdateChecker.onMainActivityCreate(this); super.onCreate(savedInstanceState); - this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> { + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> { IntentHelper.startActivity(this, SettingsActivity.class); - return true; - }, R.string.pref_category_settings); + return true; + }, R.string.pref_category_settings); setContentView(R.layout.activity_main); this.setTitle(R.string.app_name); this.getWindow().setFlags( @@ -208,7 +208,7 @@ public void commonNext() { noodleDebug.replace("Check Update"); RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat( - (int) (value * PRECISION), true) :() -> + (int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat( (int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); @@ -432,7 +432,7 @@ public void onRefresh() { final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount(); RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat( - (int) (value * PRECISION), true) :() -> + (int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat( (int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); @@ -483,7 +483,7 @@ public void onRefresh() { this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); noodleDebug.pop(); noodleDebug.unbind(); - },"Repo update thread").start(); + }, "Repo update thread").start(); } @Override @@ -532,9 +532,22 @@ private void ensurePermissions() { ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - // TODO Use standard Android API to ask for permissions - Shell.cmd("pm grant " + this.getPackageName() + " " + - Manifest.permission.POST_NOTIFICATIONS); + // Show a dialog explaining why we need this permission, which is to show + // notifications for updates + runOnUiThread(() -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.permission_notification_title); + builder.setMessage(R.string.permission_notification_message); + builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> this.requestPermissions(new String[]{ + Manifest.permission.POST_NOTIFICATIONS}, 0)); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> { + // Set pref_background_update_check to false and dismiss dialog + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("pref_background_update_check", false).apply(); + dialog.dismiss(); + }); + builder.show(); + }); } } } \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 5133a5148..75a5c21fb 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -15,6 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StyleRes; +import androidx.core.app.NotificationManagerCompat; import androidx.emoji2.text.DefaultEmojiCompatConfig; import androidx.emoji2.text.EmojiCompat; import androidx.emoji2.text.FontRequestEmojiCompatConfig; @@ -207,6 +208,10 @@ public static String formatTime(long timeStamp) { private FoxThemeWrapper markwonThemeContext; private Markwon markwon; + public static boolean isNotificationPermissionGranted() { + return NotificationManagerCompat.from(INSTANCE).areNotificationsEnabled(); + } + public Markwon getMarkwon() { if (this.markwon != null) return this.markwon; diff --git a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java index a59003aad..f9836869d 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java @@ -3,6 +3,8 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; + import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.utils.PropUtils; @@ -65,6 +67,8 @@ public CustomRepoData addRepo(String repo) { CustomRepoData customRepoData = (CustomRepoData) this.repoManager.addOrGet(repo); customRepoData.override = "custom_repo_" + i; + // Set the enabled state to true + customRepoData.setEnabled(true); customRepoData.updateEnabledState(); return customRepoData; } 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 b671d0eb3..15dfcd8c3 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -262,12 +262,18 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { Preference debugNotification = findPreference("pref_background_update_check_debug"); debugNotification.setEnabled(MainApplication.isBackgroundUpdateCheckEnabled()); debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped()); + debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped()); debugNotification.setOnPreferenceClickListener(preference -> { BackgroundUpdateChecker.postNotification(this.requireContext(), new Random().nextInt(4) + 2); return true; }); Preference backgroundUpdateCheck = findPreference("pref_background_update_check"); backgroundUpdateCheck.setVisible(!MainApplication.isWrapped()); + // Make uncheckable if POST_NOTIFICATIONS permission is not granted + if (!MainApplication.isNotificationPermissionGranted()) { + backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required); + backgroundUpdateCheck.setEnabled(false); + } backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = Boolean.parseBoolean(String.valueOf(newValue)); debugNotification.setEnabled(enabled); @@ -651,7 +657,7 @@ public void updateCustomRepoList(boolean initial) { if (preference == null) continue; final int index = i; preference.setOnPreferenceClickListener(preference1 -> { - sharedPreferences.edit().putBoolean("pref_custom_repo_" + index + "_enabled", false).apply(); + sharedPreferences.edit().remove("pref_custom_repo_" + index + "_enabled").apply(); customRepoManager.removeRepo(index); updateCustomRepoList(false); return true; @@ -677,7 +683,6 @@ public void updateCustomRepoList(boolean initial) { String text = String.valueOf(input.getText()); if (customRepoManager.canAddRepo(text)) { final CustomRepoData customRepoData = customRepoManager.addRepo(text); - customRepoData.setEnabled(true); new Thread("Add Custom Repo Thread") { @Override public void run() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b440697e..c41ad9544 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -183,4 +183,8 @@ 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. + Allow notifications? + We need the notifications permission to notify you on app and module updates. If you don\'t grant this permission, background update checks will not run. + Grant permission + Please enable notifications to enable this option. From 39cfa8c52ed95ef1c550c3fb895bbfd854d58050 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sun, 27 Nov 2022 11:50:31 -0500 Subject: [PATCH 08/19] Allow user to permanently dismiss notifications request Also handles pre-13 devices that have blocked notifications Signed-off-by: androidacy-user --- app/build.gradle | 6 +- .../java/com/fox2code/mmm/MainActivity.java | 206 +++++++++--------- .../mmm/settings/SettingsActivity.java | 25 ++- app/src/main/res/layout/dialog_checkbox.xml | 16 ++ app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 145 insertions(+), 110 deletions(-) create mode 100644 app/src/main/res/layout/dialog_checkbox.xml diff --git a/app/build.gradle b/app/build.gradle index 5ff7f8482..da2dc7e94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,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' } } diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 3641747d8..1ac0f2060 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -1,24 +1,31 @@ package com.fox2code.mmm; import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; +import android.widget.CheckBox; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.cardview.widget.CardView; +import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.preference.PreferenceManager; @@ -46,9 +53,7 @@ import eightbitlab.com.blurview.BlurView; -public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, - SearchView.OnQueryTextListener, SearchView.OnCloseListener, - OverScrollManager.OverScrollHelper { +public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper { private static final String TAG = "MainActivity"; private static final int PRECISION = 10000; public static boolean noodleDebugState = BuildConfig.DEBUG; @@ -93,9 +98,7 @@ protected void onCreate(Bundle savedInstanceState) { }, R.string.pref_category_settings); setContentView(R.layout.activity_main); this.setTitle(R.string.app_name); - this.getWindow().setFlags( - WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, - WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); setActionBarBackground(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes(); @@ -108,10 +111,8 @@ protected void onCreate(Bundle savedInstanceState) { this.actionBarBackground = new ColorDrawable(Color.TRANSPARENT); this.progressIndicator = findViewById(R.id.progress_bar); this.swipeRefreshLayout = findViewById(R.id.swipe_refresh); - this.swipeRefreshLayoutOrigStartOffset = - this.swipeRefreshLayout.getProgressViewStartOffset(); - this.swipeRefreshLayoutOrigEndOffset = - this.swipeRefreshLayout.getProgressViewEndOffset(); + this.swipeRefreshLayoutOrigStartOffset = this.swipeRefreshLayout.getProgressViewStartOffset(); + this.swipeRefreshLayoutOrigEndOffset = this.swipeRefreshLayout.getProgressViewEndOffset(); this.swipeRefreshBlocker = Long.MAX_VALUE; this.moduleList = findViewById(R.id.module_list); this.searchCard = findViewById(R.id.search_card); @@ -134,8 +135,7 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat }); this.searchCard.setRadius(this.searchCard.getHeight() / 2F); this.searchView.setMinimumHeight(FoxDisplay.dpToPixel(16)); - this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | - EditorInfo.IME_FLAG_NO_FULLSCREEN); + this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | EditorInfo.IME_FLAG_NO_FULLSCREEN); this.searchView.setOnQueryTextListener(this); this.searchView.setOnCloseListener(this); this.searchView.setOnQueryTextFocusChangeListener((v, h) -> { @@ -155,8 +155,7 @@ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newStat @Override public void onPathReceived(String path) { Log.i(TAG, "Got magisk path: " + path); - if (InstallerInitializer.peekMagiskVersion() < - Constants.MAGISK_VER_CODE_INSTALL_COMMAND) + if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND) moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED); if (!MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); @@ -166,8 +165,7 @@ public void onPathReceived(String path) { ensurePermissions(); noodleDebug.pop(); ModuleManager.getINSTANCE().scan(); - ModuleManager.getINSTANCE().runAfterScan( - moduleViewListBuilder::appendInstalledModules); + ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules); this.commonNext(); } @@ -176,8 +174,7 @@ public void onFailure(int error) { Log.i(TAG, "Failed to get magisk path!"); noodleDebug.setEnabled(noodleDebugState); noodleDebug.bind(); - moduleViewListBuilder.addNotification( - InstallerInitializer.getErrorNotification()); + moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification()); this.commonNext(); } @@ -206,11 +203,7 @@ public void commonNext() { noodleDebug.replace("Check Update Compat"); AppUpdateManager.getAppUpdateManager().checkUpdateCompat(); noodleDebug.replace("Check Update"); - RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> - progressIndicator.setProgressCompat( - (int) (value * PRECISION), true) : () -> - progressIndicator.setProgressCompat( - (int) (value * PRECISION * 0.75F), true))); + RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); if (!NotificationType.NO_INTERNET.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); @@ -224,21 +217,17 @@ public void commonNext() { if (max != 0) { int current = 0; noodleDebug.push(""); - for (LocalModuleInfo localModuleInfo : - ModuleManager.getINSTANCE().getModules().values()) { + for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { if (localModuleInfo.updateJson != null) { noodleDebug.replace(localModuleInfo.id); try { localModuleInfo.checkModuleUpdate(); } catch (Exception e) { - Log.e("MainActivity", "Failed to fetch update of: " - + localModuleInfo.id, e); + Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e); } current++; final int currentTmp = current; - runOnUiThread(() -> progressIndicator.setProgressCompat( - (int) ((1F * currentTmp / max) * PRECISION * 0.25F - + (PRECISION * 0.75F)), true)); + runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true)); } } noodleDebug.pop(); @@ -252,8 +241,7 @@ public void commonNext() { updateScreenInsets(getResources().getConfiguration()); }); noodleDebug.replace("Apply"); - RepoManager.getINSTANCE().runAfterUpdate( - moduleViewListBuilder::appendRemoteModules); + RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); noodleDebug.pop(); Log.i(TAG, "Finished app opening state!"); @@ -266,8 +254,7 @@ public void commonNext() { private void cardIconifyUpdate() { boolean iconified = this.searchView.isIconified(); - int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? - R.attr.colorSecondaryContainer : // Monet is special... + int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? R.attr.colorSecondaryContainer : // Monet is special... R.attr.colorSecondary : R.attr.colorPrimarySurface; Resources.Theme theme = this.searchCard.getContext().getTheme(); TypedValue value = new TypedValue(); @@ -277,48 +264,38 @@ private void cardIconifyUpdate() { } private void updateScreenInsets() { - this.runOnUiThread(() -> this.updateScreenInsets( - this.getResources().getConfiguration())); + this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration())); } private void updateScreenInsets(Configuration configuration) { - boolean landscape = configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE; + boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE; int bottomInset = (landscape ? 0 : this.getNavigationBarHeight()); int statusBarHeight = getStatusBarHeight(); int actionBarHeight = getActionBarHeight(); int combinedBarsHeight = statusBarHeight + actionBarHeight; this.actionBarPadding.setMinHeight(combinedBarsHeight); - this.swipeRefreshLayout.setProgressViewOffset(false, - swipeRefreshLayoutOrigStartOffset + combinedBarsHeight, - swipeRefreshLayoutOrigEndOffset + combinedBarsHeight); - this.moduleViewListBuilder.setHeaderPx(Math.max(statusBarHeight, - combinedBarsHeight - FoxDisplay.dpToPixel(4))); - this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + - bottomInset + this.searchCard.getHeight()); + this.swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayoutOrigStartOffset + combinedBarsHeight, swipeRefreshLayoutOrigEndOffset + combinedBarsHeight); + this.moduleViewListBuilder.setHeaderPx(Math.max(statusBarHeight, combinedBarsHeight - FoxDisplay.dpToPixel(4))); + this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight()); this.searchCard.setRadius(this.searchCard.getHeight() / 2F); this.moduleViewListBuilder.updateInsets(); //this.actionBarBlur.invalidate(); this.overScrollInsetTop = combinedBarsHeight; this.overScrollInsetBottom = bottomInset; - Log.d(TAG, "( " + bottomInset + ", " + - this.searchCard.getHeight() + ")"); + Log.d(TAG, "( " + bottomInset + ", " + this.searchCard.getHeight() + ")"); } private void updateBlurState() { boolean isLightMode = this.isLightTheme(); int colorBackground; try { - colorBackground = this.getColorCompat( - android.R.attr.windowBackground); + colorBackground = this.getColorCompat(android.R.attr.windowBackground); } catch (Resources.NotFoundException e) { - colorBackground = this.getColorCompat(isLightMode ? - R.color.white : R.color.black); + colorBackground = this.getColorCompat(isLightMode ? R.color.white : R.color.black); } if (MainApplication.isBlurEnabled()) { this.actionBarBlur.setBlurEnabled(true); - this.actionBarBackground.setColor(ColorUtils - .setAlphaComponent(colorBackground, 0x02)); + this.actionBarBackground.setColor(ColorUtils.setAlphaComponent(colorBackground, 0x02)); this.actionBarBackground.setColor(Color.TRANSPARENT); } else { this.actionBarBlur.setBlurEnabled(false); @@ -346,23 +323,20 @@ public void refreshUI() { InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() { @Override public void onPathReceived(String path) { - if (InstallerInitializer.peekMagiskVersion() < - Constants.MAGISK_VER_CODE_INSTALL_COMMAND) + if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND) moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED); if (!MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); noodleDebug.setEnabled(noodleDebugState); noodleDebug.bind(); ModuleManager.getINSTANCE().scan(); - ModuleManager.getINSTANCE().runAfterScan( - moduleViewListBuilder::appendInstalledModules); + ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules); this.commonNext(); } @Override public void onFailure(int error) { - moduleViewListBuilder.addNotification( - InstallerInitializer.getErrorNotification()); + moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification()); noodleDebug.setEnabled(noodleDebugState); noodleDebug.bind(); this.commonNext(); @@ -386,17 +360,14 @@ else if (AppUpdateManager.getAppUpdateManager().checkUpdate(false)) progressIndicator.setMax(PRECISION); }); noodleDebug.replace("Check Update"); - RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> - progressIndicator.setProgressCompat( - (int) (value * PRECISION), true))); + RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> progressIndicator.setProgressCompat((int) (value * PRECISION), true))); runOnUiThread(() -> { progressIndicator.setProgressCompat(PRECISION, true); progressIndicator.setVisibility(View.GONE); }); } noodleDebug.replace("Apply"); - RepoManager.getINSTANCE().runAfterUpdate( - moduleViewListBuilder::appendRemoteModules); + RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); Log.i(TAG, "Common Before applyTo"); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); noodleDebug.pop(); @@ -414,9 +385,7 @@ protected void onWindowUpdated() { @Override public void onRefresh() { - if (this.swipeRefreshBlocker > System.currentTimeMillis() || - this.initMode || this.progressIndicator == null || - this.progressIndicator.getVisibility() == View.VISIBLE) { + if (this.swipeRefreshBlocker > System.currentTimeMillis() || this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE) { this.swipeRefreshLayout.setRefreshing(false); return; // Do not double scan } @@ -430,11 +399,7 @@ public void onRefresh() { Http.cleanDnsCache(); // Allow DNS reload from network noodleDebug.push("Check Update"); final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount(); - RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> - progressIndicator.setProgressCompat( - (int) (value * PRECISION), true) : () -> - progressIndicator.setProgressCompat( - (int) (value * PRECISION * 0.75F), true))); + RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); if (!NotificationType.NO_INTERNET.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); @@ -448,21 +413,17 @@ public void onRefresh() { if (max != 0) { int current = 0; noodleDebug.push(""); - for (LocalModuleInfo localModuleInfo : - ModuleManager.getINSTANCE().getModules().values()) { + for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { if (localModuleInfo.updateJson != null) { noodleDebug.replace(localModuleInfo.id); try { localModuleInfo.checkModuleUpdate(); } catch (Exception e) { - Log.e("MainActivity", "Failed to fetch update of: " - + localModuleInfo.id, e); + Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e); } current++; final int currentTmp = current; - runOnUiThread(() -> progressIndicator.setProgressCompat( - (int) ((1F * currentTmp / max) * PRECISION * 0.25F - + (PRECISION * 0.75F)), true)); + runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true)); } } noodleDebug.pop(); @@ -478,8 +439,7 @@ public void onRefresh() { this.moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); } RepoManager.getINSTANCE().updateEnabledStates(); - RepoManager.getINSTANCE().runAfterUpdate( - moduleViewListBuilder::appendRemoteModules); + RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); noodleDebug.pop(); noodleDebug.unbind(); @@ -491,8 +451,7 @@ public boolean onQueryTextSubmit(final String query) { this.searchView.clearFocus(); if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(query)) { - new Thread(() -> this.moduleViewListBuilder.applyTo( - moduleList, moduleViewAdapter), "Query update thread").start(); + new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return true; } @@ -501,8 +460,7 @@ public boolean onQueryTextSubmit(final String query) { public boolean onQueryTextChange(String query) { if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(query)) { - new Thread(() -> this.moduleViewListBuilder.applyTo( - moduleList, moduleViewAdapter), "Query update thread").start(); + new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return false; } @@ -511,8 +469,7 @@ public boolean onQueryTextChange(String query) { public boolean onClose() { if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(null)) { - new Thread(() -> this.moduleViewListBuilder.applyTo( - moduleList, moduleViewAdapter), "Query update thread").start(); + new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return false; } @@ -527,27 +484,64 @@ public int getOverScrollInsetBottom() { return this.overScrollInsetBottom; } + @SuppressLint("RestrictedApi") private void ensurePermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - ContextCompat.checkSelfPermission(this, - Manifest.permission.POST_NOTIFICATIONS) != - PackageManager.PERMISSION_GRANTED) { - // Show a dialog explaining why we need this permission, which is to show - // notifications for updates - runOnUiThread(() -> { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.permission_notification_title); - builder.setMessage(R.string.permission_notification_message); - builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> this.requestPermissions(new String[]{ - Manifest.permission.POST_NOTIFICATIONS}, 0)); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> { - // Set pref_background_update_check to false and dismiss dialog - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - prefs.edit().putBoolean("pref_background_update_check", false).apply(); - dialog.dismiss(); + // First, check if user has said don't ask again by checking if pref_dont_ask_again_notification_permission is true + if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("pref_dont_ask_again_notification_permission", false)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + // Show a dialog explaining why we need this permission, which is to show + // notifications for updates + runOnUiThread(() -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.permission_notification_title); + builder.setMessage(R.string.permission_notification_message); + // Don't ask again checkbox + View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null); + CheckBox checkBox = view.findViewById(R.id.checkbox); + checkBox.setText(R.string.dont_ask_again); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply()); + builder.setView(view); + builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> { + // Request the permission + this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0); + }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> { + // Set pref_background_update_check to false and dismiss dialog + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("pref_background_update_check", false).apply(); + dialog.dismiss(); + }); + builder.show(); }); - builder.show(); - }); + // Next branch is for < android 13 and user has blocked notifications + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(this).areNotificationsEnabled()) { + runOnUiThread(() -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.permission_notification_title); + builder.setMessage(R.string.permission_notification_message); + // Don't ask again checkbox + View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null); + CheckBox checkBox = view.findViewById(R.id.checkbox); + checkBox.setText(R.string.dont_ask_again); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply()); + builder.setView(view); + builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> { + // Open notification settings + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> { + // Set pref_background_update_check to false and dismiss dialog + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("pref_background_update_check", false).apply(); + dialog.dismiss(); + }); + builder.show(); + }); + } } } } \ No newline at end of file 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 15dfcd8c3..89425651f 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -9,10 +9,12 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.provider.Settings; import android.util.Log; import android.view.inputmethod.EditorInfo; import android.widget.AutoCompleteTextView; @@ -271,8 +273,29 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { backgroundUpdateCheck.setVisible(!MainApplication.isWrapped()); // Make uncheckable if POST_NOTIFICATIONS permission is not granted if (!MainApplication.isNotificationPermissionGranted()) { + // Instead of disabling the preference, we make it uncheckable and when the user + // clicks on it, we show a dialog explaining why the permission is needed + backgroundUpdateCheck.setOnPreferenceClickListener(preference -> { + // set the box to unchecked + ((SwitchPreferenceCompat) backgroundUpdateCheck).setChecked(false); + // ensure that the preference is false + MainApplication.getSharedPreferences().edit().putBoolean("pref_background_update_check", false).apply(); + new MaterialAlertDialogBuilder(this.requireContext()) + .setTitle(R.string.permission_notification_title) + .setMessage(R.string.permission_notification_message) + .setPositiveButton(R.string.ok, (dialog, which) -> { + // Open the app settings + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", this.requireContext().getPackageName(), null); + intent.setData(uri); + this.startActivity(intent); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> {}) + .show(); + return true; + }); backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required); - backgroundUpdateCheck.setEnabled(false); } backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = Boolean.parseBoolean(String.valueOf(newValue)); diff --git a/app/src/main/res/layout/dialog_checkbox.xml b/app/src/main/res/layout/dialog_checkbox.xml new file mode 100644 index 000000000..a50699ff2 --- /dev/null +++ b/app/src/main/res/layout/dialog_checkbox.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 26011f60a..198916c00 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -11,4 +11,5 @@ Dark Light + Don't prompt again \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c41ad9544..cb7c4f57e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,4 +187,5 @@ We need the notifications permission to notify you on app and module updates. If you don\'t grant this permission, background update checks will not run. Grant permission Please enable notifications to enable this option. + Don\'t ask again From 29e3d7e58e27509c6eb5708d90066f8cfa17551a Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Sun, 27 Nov 2022 21:19:39 -0500 Subject: [PATCH 09/19] Add option to make dark theme a black theme Signed-off-by: androidacy-user --- .../java/com/fox2code/mmm/MainApplication.java | 11 +++++++++++ .../fox2code/mmm/settings/SettingsActivity.java | 14 ++++++++++++++ app/src/main/res/values-v31/themes.xml | 10 ++++++++++ app/src/main/res/values/strings.xml | 3 +++ app/src/main/res/values/styles.xml | 5 +++++ app/src/main/res/values/themes.xml | 10 ++++++++++ app/src/main/res/xml/root_preferences.xml | 9 +++++++++ 7 files changed, 62 insertions(+) create mode 100644 app/src/main/res/values/styles.xml diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 75a5c21fb..5fbfea797 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -295,9 +295,20 @@ public void updateTheme() { R.style.Theme_MagiskModuleManager_Light; break; } + // Handle force black theme + if (theme.equals("dark") && isForceBlackThemeEnabled()) { + // just black background + themeResId = monet ? + R.style.Theme_MagiskModuleManager_Monet_Black : + R.style.Theme_MagiskModuleManager_Black; + } this.setManagerThemeResId(themeResId); } + private boolean isForceBlackThemeEnabled() { + return getSharedPreferences().getBoolean("pref_force_black_theme", false); + } + @StyleRes public int getManagerThemeResId() { return managerThemeResId; 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 89425651f..ef0ee4355 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -137,6 +137,20 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }, 1); return true; }); + // Force black theme - depends on themePreference being system or dark + SwitchPreferenceCompat forceBlackPreference = findPreference("pref_force_black_theme"); + // Set invisible if theme is not dark or system + forceBlackPreference.setVisible(themePreference.getValue().equals("dark") || + themePreference.getValue().equals("system")); + forceBlackPreference.setOnPreferenceChangeListener((preference, newValue) -> { + devModeStep = 0; + UiThreadHandler.handler.postDelayed(() -> { + MainApplication.getINSTANCE().updateTheme(); + FoxActivity.getFoxActivity(this).setThemeRecreate( + MainApplication.getINSTANCE().getManagerThemeResId()); + }, 1); + return true; + }); // Crash reporting TwoStatePreference crashReportingPreference = findPreference("pref_crash_reporting"); if (!SentryMain.IS_SENTRY_INSTALLED) crashReportingPreference.setVisible(false); diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index b70691e2f..e7fb75bb8 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -22,6 +22,16 @@ @style/Widget.Material3.Chip.Choice.Dark + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 00b67dae3..82578a62a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -83,6 +83,16 @@ @style/Widget.Material.Chip.Choice.Dark + + +