diff --git a/.gitignore b/.gitignore index 71f7189ae..fea47f90d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .cxx local.properties sentry.properties +androidacy.properties \ No newline at end of file diff --git a/README.md b/README.md index 383b714d3..1b5ea4e46 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) @@ -94,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) @@ -112,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: @@ -152,6 +158,13 @@ If your language is right to left don't forget to set `lang_support_rtl` to `tru Translators are not expected to have any previous coding experience. +## License +See [LICENSE](LICENCE). Library licenses can be found in the licenses section of the app. + +Cronet is licensed under the Apache License, Version 2.0. Static libraries are licensed under +the BSD license. See [LICENSE](https://chromium.googlesource.com/chromium/src/+/master/LICENSE) +for more information. Libraries were built using the microg build script which can be found [here](https://github.com/microg/cronet-build). + ## I want to add my own repo To add you own repo to Fox's mmm it need to follow theses conditions: diff --git a/app/build.gradle b/app/build.gradle index dab0edcee..268b1f309 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,29 +8,46 @@ plugins { android { namespace "com.fox2code.mmm" compileSdk 33 + buildToolsVersion '30.0.3' + signingConfigs { + release { + // Everything comes from local.properties + Properties properties = new Properties() + if (project.rootProject.file('local.properties').exists()) { + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + storeFile file(properties.getProperty('keystore.file')) + storePassword properties.getProperty('keystore.password') + keyAlias 'key0' + keyPassword properties.getProperty('keystore.password') + } + } + } 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" + signingConfig signingConfigs.release } buildTypes { release { minifyEnabled true shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), +'proguard-rules.pro' } debug { applicationIdSuffix '.debug' debuggable true + // ONLY FOR TESTING SENTRY // minifyEnabled true // shrinkResources true - // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro' } } @@ -40,9 +57,26 @@ android { dimension "type" buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "true" buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true" - buildConfigField("java.util.List", - "ENABLED_REPOS", - "java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",) + // Get the androidacy client ID from the androidacy.properties + Properties properties = new Properties() + // If androidacy.properties doesn't exist, use the default client ID which is heavily + // rate limited to 30 requests per minute + if (project.rootProject.file('androidacy.properties').exists()) { + properties.load(project.rootProject.file('androidacy.properties').newDataInputStream()) + } else { + properties.setProperty('client_id', '"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"') + } + buildConfigField("String", "ANDROIDACY_CLIENT_ID", properties.getProperty('client_id')) + // If client ID is empty, disable androidacy + if (properties.getProperty('client_id').isEmpty()) { + buildConfigField("java.util.List", + "ENABLED_REPOS", "java.util.Arrays.asList(\"magisk_alt_repo\")") + } else { + buildConfigField("java.util.List", + "ENABLED_REPOS", + "java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",) + } + } fdroid { @@ -62,6 +96,17 @@ android { buildConfigField("java.util.List", "ENABLED_REPOS", "java.util.Arrays.asList(\"magisk_alt_repo\")",) + + // Get the androidacy client ID from the androidacy.properties + Properties properties = new Properties() + // If androidacy.properties doesn't exist, use the default client ID which is limited + // to 50 requests per minute + if (project.rootProject.file('androidacy.properties').exists()) { + properties.load(project.rootProject.file('androidacy.properties').newDataInputStream()) + } else { + properties.setProperty('client_id', '"dQ1p7X8bF14PVJ7wAU6ORVjPB2IeTinsuAZ8Uos6tQiyUdUyIjSyZSmN54QBbaTy"') + } + buildConfigField("String", "ANDROIDACY_CLIENT_ID", properties.getProperty('client_id')) } } @@ -137,7 +182,7 @@ sentry { // as Gradle will resolve it to the latest version. // // Defaults to the latest published sentry version. - sentryVersion = '6.5.0' + sentryVersion = '6.9.2' } } @@ -175,17 +220,20 @@ 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' + // Chromium cronet from microG + implementation fileTree(dir: 'libs', include: '*.jar') + // Force prefer our own version of Cronet 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.9.2' + defaultImplementation 'io.sentry:sentry-android-fragment:6.9.2' + defaultImplementation 'io.sentry:sentry-android-okhttp:6.9.2' + defaultImplementation 'io.sentry:sentry-android-core:6.9.2' + defaultImplementation 'io.sentry:sentry-android-ndk:6.9.2' } // Markdown @@ -193,13 +241,14 @@ 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.net.cronet:cronet-okhttp:0.1.0' + // Ignore all org.chromium.net dependencies 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/libs/arm64-v8a.jar b/app/libs/arm64-v8a.jar new file mode 100644 index 000000000..315c2b85e Binary files /dev/null and b/app/libs/arm64-v8a.jar differ diff --git a/app/libs/armeabi-v7a.jar b/app/libs/armeabi-v7a.jar new file mode 100644 index 000000000..c3c8f84ff Binary files /dev/null and b/app/libs/armeabi-v7a.jar differ diff --git a/app/libs/cronet_impl_common_java.jar b/app/libs/cronet_impl_common_java.jar new file mode 100644 index 000000000..cc492a615 Binary files /dev/null and b/app/libs/cronet_impl_common_java.jar differ diff --git a/app/libs/cronet_impl_native_java.jar b/app/libs/cronet_impl_native_java.jar new file mode 100644 index 000000000..ecb9d8a92 Binary files /dev/null and b/app/libs/cronet_impl_native_java.jar differ diff --git a/app/libs/x86.jar b/app/libs/x86.jar new file mode 100644 index 000000000..8a1556c8e Binary files /dev/null and b/app/libs/x86.jar differ diff --git a/app/libs/x86_64.jar b/app/libs/x86_64.jar new file mode 100644 index 000000000..6d45fadb7 Binary files /dev/null and b/app/libs/x86_64.jar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2a40c084d..a70b78107 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 @@ -186,3 +186,37 @@ int getSafeInsetTop(); android.graphics.Insets getWaterfallInsets(); } + +# Keep all of Cronet API and google's internal classes +-keep class org.chromium.net.** { *; } +-keep class org.chromium.** { *; } +-keep class com.google.** { *; } + +# 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 +-dontwarn com.afollestad.materialdialogs.MaterialDialog +-dontwarn com.afollestad.materialdialogs.WhichButton +-dontwarn com.afollestad.materialdialogs.actions.DialogActionExtKt +-dontwarn com.afollestad.materialdialogs.callbacks.DialogCallbackExtKt +-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButton +-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout +-dontwarn com.afollestad.materialdialogs.internal.main.DialogLayout +-dontwarn com.afollestad.materialdialogs.internal.main.DialogTitleLayout +-dontwarn com.afollestad.materialdialogs.internal.message.DialogContentLayout +-dontwarn com.oracle.svm.core.annotate.AutomaticFeature +-dontwarn com.oracle.svm.core.annotate.Delete +-dontwarn com.oracle.svm.core.annotate.Substitute +-dontwarn com.oracle.svm.core.annotate.TargetClass +-dontwarn com.oracle.svm.core.configure.ResourcesRegistry +-dontwarn javax.lang.model.element.Modifier +-dontwarn org.graalvm.nativeimage.ImageSingletons +-dontwarn org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess +-dontwarn org.graalvm.nativeimage.hosted.Feature \ 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/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 147169ade..34d11e363 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -1,25 +1,33 @@ 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.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; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -39,16 +47,12 @@ 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, - 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; @@ -87,15 +91,13 @@ 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( - 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 +110,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 +134,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 +154,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 +164,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 +173,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,14 +202,12 @@ 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); + } else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) { + moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED); } else { // Compatibility data still needs to be updated AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager(); @@ -224,21 +218,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 +242,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 +255,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 +265,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 +324,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 +361,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 +386,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,45 +400,37 @@ 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); } else { // Compatibility data still needs to be updated AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager(); - noodleDebug.replace("Check App Update"); + // noodleDebug.replace("Check App Update"); if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true)) moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); - noodleDebug.replace("Check Json Update"); + // noodleDebug.replace("Check Json Update"); 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(); } } - noodleDebug.replace("Apply"); + // noodleDebug.replace("Apply"); runOnUiThread(() -> { this.progressIndicator.setVisibility(View.GONE); this.swipeRefreshLayout.setRefreshing(false); @@ -478,12 +440,11 @@ 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(); - },"Repo update thread").start(); + // noodleDebug.pop(); + // noodleDebug.unbind(); + }, "Repo update thread").start(); } @Override @@ -491,8 +452,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 +461,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 +470,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,14 +485,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) { - // TODO Use standard Android API to ask for permissions - Shell.cmd("pm grant " + this.getPackageName() + " " + - Manifest.permission.POST_NOTIFICATIONS); + // 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(); + }); + // 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/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index e70319ea4..dac051be0 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; @@ -14,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; @@ -32,6 +34,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Objects; import java.util.Random; import io.noties.markwon.Markwon; @@ -45,35 +48,33 @@ import io.noties.prism4j.Prism4j; import io.noties.prism4j.annotations.PrismBundle; -@PrismBundle( - includeAll = true, - grammarLocatorClassName = ".Prism4jGrammarLocator" -) -public class MainApplication extends FoxApplication - implements androidx.work.Configuration.Provider { +@PrismBundle(includeAll = true, grammarLocatorClassName = ".Prism4jGrammarLocator") +public class MainApplication extends FoxApplication implements androidx.work.Configuration.Provider { private static final String TAG = "MainApplication"; private static final String timeFormatString = "dd MMM yyyy"; // Example: 13 july 2001 - private static Locale timeFormatLocale = - Resources.getSystem().getConfiguration().locale; - private static SimpleDateFormat timeFormat = - new SimpleDateFormat(timeFormatString, timeFormatLocale); private static final Shell.Builder shellBuilder; private static final long secret; @SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper. private static final boolean wrapped = !FoxProcessExt.isRootLoader(); + private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().locale; + private static SimpleDateFormat timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale); private static SharedPreferences bootSharedPreferences; private static String relPackageName = BuildConfig.APPLICATION_ID; private static MainApplication INSTANCE; private static boolean firstBoot; static { - Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create() - .setFlags(Shell.FLAG_REDIRECT_STDERR) - .setTimeout(10).setInitializers(InstallerInitializer.class) - ); + Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR).setTimeout(10).setInitializers(InstallerInitializer.class)); secret = new Random().nextLong(); } + // Provides the Context for the base application + public Context FoxApplication = this; + @StyleRes + private int managerThemeResId = R.style.Theme_MagiskModuleManager; + private FoxThemeWrapper markwonThemeContext; + private Markwon markwon; + public MainApplication() { if (INSTANCE != null && INSTANCE != this) throw new IllegalStateException("Duplicate application instance!"); @@ -86,10 +87,8 @@ public static Shell build(String... command) { public static void addSecret(Intent intent) { ComponentName componentName = intent.getComponent(); - String packageName = componentName != null ? - componentName.getPackageName() : intent.getPackage(); - if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) && - !relPackageName.equals(packageName)) { + String packageName = componentName != null ? componentName.getPackageName() : intent.getPackage(); + if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) && !relPackageName.equals(packageName)) { // Code safeguard, we should never reach here. throw new IllegalArgumentException("Can't add secret to outbound Intent"); } @@ -135,29 +134,23 @@ public static boolean isDohEnabled() { } public static boolean isMonetEnabled() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - getSharedPreferences().getBoolean("pref_enable_monet", true); + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences().getBoolean("pref_enable_monet", true); } public static boolean isBlurEnabled() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - getSharedPreferences().getBoolean("pref_enable_blur", false); + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getSharedPreferences().getBoolean("pref_enable_blur", false); } public static boolean isDeveloper() { - return BuildConfig.DEBUG || - getSharedPreferences().getBoolean("developer", false); + return BuildConfig.DEBUG || getSharedPreferences().getBoolean("developer", false); } public static boolean isDisableLowQualityModuleFilter() { - return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter", - false) && isDeveloper(); + return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper(); } public static boolean isUsingMagiskCommand() { - return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND - && getSharedPreferences().getBoolean("pref_use_magisk_install_command", false) - && isDeveloper(); + return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && getSharedPreferences().getBoolean("pref_use_magisk_install_command", false) && isDeveloper(); } public static boolean isBackgroundUpdateCheckEnabled() { @@ -165,8 +158,7 @@ public static boolean isBackgroundUpdateCheckEnabled() { } public static boolean isAndroidacyTestMode() { - return isDeveloper() && - getSharedPreferences().getBoolean("pref_androidacy_test_mode", false); + return isDeveloper() && getSharedPreferences().getBoolean("pref_androidacy_test_mode", false); } public static boolean isFirstBoot() { @@ -182,8 +174,7 @@ public static void setHasGottenRootAccess(boolean bool) { } public static boolean isCrashReportingEnabled() { - return getSharedPreferences().getBoolean("pref_crash_reporting", - BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING && !BuildConfig.DEBUG); + return getSharedPreferences().getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING); } public static SharedPreferences getBootSharedPreferences() { @@ -199,24 +190,17 @@ public static String formatTime(long timeStamp) { return timeFormat.format(new Date(timeStamp)); } - @StyleRes - private int managerThemeResId = R.style.Theme_MagiskModuleManager; - 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; + if (this.markwon != null) return this.markwon; FoxThemeWrapper contextThemeWrapper = this.markwonThemeContext; if (contextThemeWrapper == null) { - contextThemeWrapper = this.markwonThemeContext = - new FoxThemeWrapper(this, this.managerThemeResId); + contextThemeWrapper = this.markwonThemeContext = new FoxThemeWrapper(this, this.managerThemeResId); } - Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create()) - .usePlugin(SyntaxHighlightPlugin.create( - new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme())) - .usePlugin(ImagesPlugin.create().addSchemeHandler( - OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build(); + Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create()).usePlugin(SyntaxHighlightPlugin.create(new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme())).usePlugin(ImagesPlugin.create().addSchemeHandler(OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build(); return this.markwon = markwon; } @@ -230,40 +214,6 @@ public androidx.work.Configuration getWorkManagerConfiguration() { return new androidx.work.Configuration.Builder().build(); } - private class Prism4jSwitchTheme implements Prism4jTheme { - private final Prism4jTheme light = new Prism4jThemeDefault(Color.TRANSPARENT); - private final Prism4jTheme dark = new Prism4jThemeDarkula(Color.TRANSPARENT); - - private Prism4jTheme getTheme() { - return isLightTheme() ? this.light : this.dark; - } - - @Override - public int background() { - return this.getTheme().background(); - } - - @Override - public int textColor() { - return this.getTheme().textColor(); - } - - @Override - public void apply(@NonNull String language, @NonNull Prism4j.Syntax syntax, - @NonNull SpannableStringBuilder builder, int start, int end) { - this.getTheme().apply(language, syntax, builder, start, end); - } - } - - @SuppressLint("NonConstantResourceId") - public void setManagerThemeResId(@StyleRes int resId) { - this.managerThemeResId = resId; - if (this.markwonThemeContext != null) { - this.markwonThemeContext.setTheme(resId); - } - this.markwon = null; - } - public void updateTheme() { @StyleRes int themeResId; String theme; @@ -272,19 +222,16 @@ public void updateTheme() { default: Log.w("MainApplication", "Unknown theme id: " + theme); case "system": - themeResId = monet ? - R.style.Theme_MagiskModuleManager_Monet : - R.style.Theme_MagiskModuleManager; + themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet : R.style.Theme_MagiskModuleManager; break; case "dark": - themeResId = monet ? - R.style.Theme_MagiskModuleManager_Monet_Dark : - R.style.Theme_MagiskModuleManager_Dark; + themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Dark : R.style.Theme_MagiskModuleManager_Dark; + break; + case "black": + themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Black : R.style.Theme_MagiskModuleManager_Black; break; case "light": - themeResId = monet ? - R.style.Theme_MagiskModuleManager_Monet_Light : - R.style.Theme_MagiskModuleManager_Light; + themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Light : R.style.Theme_MagiskModuleManager_Light; break; } this.setManagerThemeResId(themeResId); @@ -295,14 +242,21 @@ public int getManagerThemeResId() { return managerThemeResId; } + @SuppressLint("NonConstantResourceId") + public void setManagerThemeResId(@StyleRes int resId) { + this.managerThemeResId = resId; + if (this.markwonThemeContext != null) { + this.markwonThemeContext.setTheme(resId); + } + this.markwon = null; + } + @SuppressLint("NonConstantResourceId") public boolean isLightTheme() { switch (this.managerThemeResId) { case R.style.Theme_MagiskModuleManager: case R.style.Theme_MagiskModuleManager_Monet: - return (this.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK) - != Configuration.UI_MODE_NIGHT_YES; + return (this.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES; case R.style.Theme_MagiskModuleManager_Monet_Light: case R.style.Theme_MagiskModuleManager_Light: return true; @@ -314,6 +268,11 @@ public boolean isLightTheme() { } } + @SuppressLint("NonConstantResourceId") + public boolean isDarkTheme() { + return !this.isLightTheme(); + } + @Override public void onCreate() { if (INSTANCE == null) INSTANCE = this; @@ -326,14 +285,12 @@ public void onCreate() { }*/ SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); // We are only one process so it's ok to do this - SharedPreferences bootPrefs = MainApplication.bootSharedPreferences = - this.getSharedPreferences("mmm_boot", MODE_PRIVATE); + SharedPreferences bootPrefs = MainApplication.bootSharedPreferences = this.getSharedPreferences("mmm_boot", MODE_PRIVATE); long lastBoot = System.currentTimeMillis() - SystemClock.elapsedRealtime(); long lastBootPrefs = bootPrefs.getLong("last_boot", 0); if (lastBootPrefs == 0 || Math.abs(lastBoot - lastBootPrefs) > 100) { boolean firstBoot = sharedPreferences.getBoolean("first_boot", true); - bootPrefs.edit().clear().putLong("last_boot", lastBoot) - .putBoolean("first_boot", firstBoot).apply(); + bootPrefs.edit().clear().putLong("last_boot", lastBoot).putBoolean("first_boot", firstBoot).apply(); if (firstBoot) { sharedPreferences.edit().putBoolean("first_boot", false).apply(); } @@ -347,12 +304,10 @@ public void onCreate() { // Update SSL Ciphers if update is possible GMSProviderInstaller.installIfNeeded(this); // Update emoji config - FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = - DefaultEmojiCompatConfig.create(this); + FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = DefaultEmojiCompatConfig.create(this); if (fontRequestEmojiCompatConfig != null) { fontRequestEmojiCompatConfig.setReplaceAll(true); - fontRequestEmojiCompatConfig - .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL); + fontRequestEmojiCompatConfig.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL); EmojiCompat emojiCompat = EmojiCompat.init(fontRequestEmojiCompatConfig); new Thread(() -> { Log.d("MainApplication", "Loading emoji compat..."); @@ -362,6 +317,13 @@ public void onCreate() { } SentryMain.initialize(this); + if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { + Log.w("MainApplication", "Androidacy client id is empty! Please set it in androidacy" + + ".properties. Will not enable Androidacy."); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean("pref_androidacy_repo_enabled", false); + editor.apply(); + } } @Override @@ -381,9 +343,35 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) { Locale newTimeFormatLocale = newConfig.locale; if (timeFormatLocale != newTimeFormatLocale) { timeFormatLocale = newTimeFormatLocale; - timeFormat = new SimpleDateFormat( - timeFormatString, timeFormatLocale); + timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale); } super.onConfigurationChanged(newConfig); } + + private class Prism4jSwitchTheme implements Prism4jTheme { + private final Prism4jTheme light = new Prism4jThemeDefault(Color.TRANSPARENT); + private final Prism4jTheme dark = new Prism4jThemeDarkula(Color.TRANSPARENT); + // Black theme + private final Prism4jTheme black = new Prism4jThemeDefault(Color.BLACK); + + private Prism4jTheme getTheme() { + // isLightTheme() means light, isDarkTheme() means dark, and isBlackTheme() means black + return isLightTheme() ? light : isDarkTheme() ? dark : black; + } + + @Override + public int background() { + return this.getTheme().background(); + } + + @Override + public int textColor() { + return this.getTheme().textColor(); + } + + @Override + public void apply(@NonNull String language, @NonNull Prism4j.Syntax syntax, @NonNull SpannableStringBuilder builder, int start, int end) { + this.getTheme().apply(language, syntax, builder, start, end); + } + } } diff --git a/app/src/main/java/com/fox2code/mmm/NotificationType.java b/app/src/main/java/com/fox2code/mmm/NotificationType.java index 999e845d6..abd4c17a3 100644 --- a/app/src/main/java/com/fox2code/mmm/NotificationType.java +++ b/app/src/main/java/com/fox2code/mmm/NotificationType.java @@ -53,9 +53,7 @@ public boolean shouldRemove() { return InstallerInitializer.getErrorNotification() != this; } }, - MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> { - IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases"); - }) { + MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases")) { @Override public boolean shouldRemove() { return InstallerInitializer.peekMagiskPath() == null || @@ -70,6 +68,12 @@ public boolean shouldRemove() { RepoManager.getINSTANCE().hasConnectivity(); } }, + REPO_UPDATE_FAILED(R.string.repo_update_failed, R.drawable.ic_baseline_cloud_off_24) { + @Override + public boolean shouldRemove() { + return RepoManager.getINSTANCE().isLastUpdateSuccess(); + } + }, NEED_CAPTCHA_ANDROIDACY(R.string.androidacy_need_captcha, R.drawable.ic_baseline_refresh_24, v -> IntentHelper.openUrlAndroidacy(v.getContext(), "https://" + Http.needCaptchaAndroidacyHost() + "/", false)) { @@ -86,10 +90,8 @@ public boolean shouldRemove() { } }, UPDATE_AVAILABLE(R.string.app_update_available, R.drawable.ic_baseline_system_update_24, - R.attr.colorPrimary, R.attr.colorOnPrimary, v -> { - IntentHelper.openUrl(v.getContext(), - "https://github.com/Fox2Code/FoxMagiskModuleManager/releases"); - }, false) { + R.attr.colorPrimary, R.attr.colorOnPrimary, v -> IntentHelper.openUrl(v.getContext(), + "https://github.com/Fox2Code/FoxMagiskModuleManager/releases"), false) { @Override public boolean shouldRemove() { return !AppUpdateManager.getAppUpdateManager().peekShouldUpdate(); 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..e26387b43 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -37,13 +37,11 @@ import com.fox2code.mmm.utils.IntentHelper; import com.google.android.material.progressindicator.LinearProgressIndicator; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; /** @@ -74,8 +72,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 +96,25 @@ 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; + } + // Add device_id to url if not present + String device_id = uri.getQueryParameter("device_id"); + if (device_id == null) { + // get from shared preferences + try { + device_id = AndroidacyRepoData.generateDeviceId(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + url = url + "&device_id=" + device_id; + } + 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 +133,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 +155,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 +165,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 +189,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 +210,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 +224,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 +232,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 +273,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 +310,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 +340,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 +361,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 +377,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 +400,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..e9d8f0927 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -1,11 +1,13 @@ package com.fox2code.mmm.androidacy; import android.content.SharedPreferences; +import android.os.Looper; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.manager.ModuleInfo; @@ -15,7 +17,8 @@ import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.HttpException; import com.fox2code.mmm.utils.PropUtils; -import com.topjohnwu.superuser.internal.UiThreadHandler; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.topjohnwu.superuser.Shell; import org.json.JSONArray; import org.json.JSONException; @@ -23,10 +26,12 @@ import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import okhttp3.HttpUrl; @@ -41,11 +46,14 @@ public final class AndroidacyRepoData extends RepoData { OK_HTTP_URL_BUILDER.build(); } + @SuppressWarnings("unused") + public final String ClientID = BuildConfig.ANDROIDACY_CLIENT_ID; + private final boolean testMode; private final String host; + public String token = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).getString("pref_androidacy_api_token", null); // Avoid spamming requests to Androidacy private long androidacyBlockade = 0; - private 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); @@ -75,15 +83,68 @@ private static String filterURL(String url) { return url; } - public boolean isValidToken(String token) throws IOException { + // Generates a unique device ID. This is used to identify the device in the API for rate + // limiting and fraud detection. + public static String generateDeviceId() throws NoSuchAlgorithmException { + // Try to get the device ID from the shared preferences + SharedPreferences sharedPreferences = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0); + String deviceIdPref = sharedPreferences.getString("device_id", null); + if (deviceIdPref != null) { + return deviceIdPref; + } else { + // AAAA we're fingerprintiiiiing + // Really not that scary - just hashes some device info. We can't even get the info + // we originally hashed, so it's not like we can use it to track you. + String deviceId = null; + // Get ro.serialno if it exists + // First, we need to get an su shell + Shell.Result result = Shell.cmd("getprop ro.serialno").exec(); + // Check if the command was successful + if (result.isSuccess()) { + // Get the output + String output = result.getOut().get(0); + // Check if the output is valid + if (output != null && !output.isEmpty()) { + deviceId = output; + } + } + // Now, get device model, manufacturer, and Android version + String deviceModel = android.os.Build.MODEL; + String deviceManufacturer = android.os.Build.MANUFACTURER; + String androidVersion = android.os.Build.VERSION.RELEASE; + // Append it all together + deviceId += deviceModel + deviceManufacturer + androidVersion; + // Hash it + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(deviceId.getBytes()); + // Convert it to a hex string + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + // Save it to shared preferences + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("device_id", hexString.toString()); + editor.apply(); + // Return it + return hexString.toString(); + } + } + + public boolean isValidToken(String token) throws IOException, NoSuchAlgorithmException { + String deviceId = generateDeviceId(); try { - Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token, false); + Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token + "&device_id=" + deviceId, false); } catch (HttpException e) { if (e.getErrorCode() == 401) { Log.w(TAG, "Invalid token, resetting..."); // Remove saved preference - SharedPreferences.Editor editor = this.cachedPreferences.edit(); - editor.remove("androidacy_api_token"); + SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit(); + editor.remove("pref_androidacy_api_token"); editor.apply(); return false; } @@ -94,7 +155,14 @@ public boolean isValidToken(String token) throws IOException { } @Override - protected boolean prepare() { + protected boolean prepare() throws NoSuchAlgorithmException { + // If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return + if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { + SharedPreferences.Editor editor = this.cachedPreferences.edit(); + editor.putBoolean("pref_androidacy_repo_enabled", false); + editor.apply(); + return false; + } if (Http.needCaptchaAndroidacy()) return false; // Implementation details discussed on telegram // First, ping the server to check if it's alive @@ -103,12 +171,18 @@ protected boolean prepare() { } catch (Exception e) { Log.e(TAG, "Failed to ping server", e); // Inform user - /*if (!HttpException.shouldTimeout(e)) { - UiThreadHandler.run(() -> Toast.makeText(MainApplication.getINSTANCE(), - R.string.androidacy_server_down, Toast.LENGTH_SHORT).show()); - }*/ + Looper.prepare(); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(MainApplication.getINSTANCE().getBaseContext()); + builder.setTitle("Androidacy Server Down"); + builder.setMessage("The Androidacy server is down. Unfortunately, this means that you" + + " will not be able to download or view modules from the Androidacy repository" + + ". Please try again later."); + builder.setPositiveButton("OK", (dialog, which) -> dialog.dismiss()); + builder.show(); + Looper.loop(); return false; } + String deviceId = generateDeviceId(); long time = System.currentTimeMillis(); if (this.androidacyBlockade > time) return false; this.androidacyBlockade = time + 30_000L; @@ -117,8 +191,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 +209,9 @@ protected boolean prepare() { } if (token == null) { try { - Log.i(TAG, "Refreshing 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); + Log.i(TAG, "Requesting new token..."); + // POST json request to https://production-api.androidacy.com/auth/register + token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "{\"device_id\":\"" + deviceId + "\"}", false)); // Parse token try { JSONObject jsonObject = new JSONObject(token); @@ -151,7 +230,9 @@ protected boolean prepare() { return false; } // Save token to shared preference - MainApplication.getSharedPreferences().edit().putString("pref_androidacy_api_token", token).apply(); + SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).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); @@ -167,7 +248,7 @@ protected boolean prepare() { } @Override - protected List populate(JSONObject jsonObject) throws JSONException { + protected List populate(JSONObject jsonObject) throws JSONException, NoSuchAlgorithmException { if (!jsonObject.getString("status").equals("success")) throw new JSONException("Response is not a success!"); String name = jsonObject.optString("name", "Androidacy Modules Repo"); @@ -280,11 +361,12 @@ public boolean tryLoadMetadata(RepoModule repoModule) { } @Override - public String getUrl() { - return this.token == null ? this.url : this.url + "?token=" + this.token; + public String getUrl() throws NoSuchAlgorithmException { + return this.token == null ? this.url : + this.url + "?token=" + this.token + "&v=" + BuildConfig.VERSION_CODE + "&c=" + BuildConfig.VERSION_NAME + "&device_id=" + generateDeviceId(); } - private String injectToken(String url) { + private String injectToken(String url) throws NoSuchAlgorithmException { // Do not inject token for non Androidacy urls if (!AndroidacyUtil.isAndroidacyLink(url)) return url; if (this.testMode) { @@ -299,6 +381,7 @@ private String injectToken(String url) { } } String token = "token=" + this.token; + String deviceId = "device_id=" + generateDeviceId(); if (!url.contains(token)) { if (url.lastIndexOf('/') < url.lastIndexOf('?')) { return url + '&' + token; @@ -306,6 +389,13 @@ private String injectToken(String url) { return url + '?' + token; } } + if (!url.contains(deviceId)) { + if (url.lastIndexOf('/') < url.lastIndexOf('?')) { + return url + '&' + deviceId; + } else { + return url + '?' + deviceId; + } + } return url; } @@ -315,10 +405,6 @@ public String getName() { return this.testMode ? super.getName() + " (Test Mode)" : super.getName(); } - String getToken() { - return this.token; - } - public void setToken(String token) { if (Http.hasWebView()) { this.token = token; 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/CustomRepoData.java b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java index 94ae43434..e0448f083 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoData.java @@ -10,6 +10,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; public final class CustomRepoData extends RepoData { boolean loadedExternal; @@ -30,12 +31,7 @@ public String getPreferenceId() { this.id : this.override; } - @Override - public boolean isLimited() { - return true; - } - - public void quickPrePopulate() throws IOException, JSONException { + public void quickPrePopulate() throws IOException, JSONException, NoSuchAlgorithmException { JSONObject jsonObject = new JSONObject( new String(Http.doHttpGet(this.getUrl(), false), StandardCharsets.UTF_8)); 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/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index ecd2567bc..17ea670b7 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -21,13 +21,13 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; 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 +54,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,17 +73,20 @@ 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"); + } } } } } - protected boolean prepare() { + protected boolean prepare() throws NoSuchAlgorithmException { return true; } - protected List populate(JSONObject jsonObject) throws JSONException { + protected List populate(JSONObject jsonObject) throws JSONException, NoSuchAlgorithmException { List newModules = new ArrayList<>(); synchronized (this.populateLock) { String name = jsonObject.getString("name").trim(); @@ -140,7 +146,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 +188,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; @@ -204,14 +216,10 @@ public void updateEnabledState() { .getBoolean("pref_" + this.getPreferenceId() + "_enabled", this.isEnabledByDefault()); } - public String getUrl() { + public String getUrl() throws NoSuchAlgorithmException { return this.url; } - public boolean isLimited() { - return false; - } - public String getPreferenceId() { return this.id; } 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..663b0ce0f 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -19,6 +19,9 @@ import com.fox2code.mmm.utils.SyncManager; import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; @@ -27,37 +30,74 @@ import java.util.List; public final class RepoManager extends SyncManager { - private static final String TAG = "RepoManager"; - - private static final String MAGISK_REPO_MANAGER = - "https://magisk-modules-repo.github.io/submission/modules.json"; public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json"; public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo"; - public static final String MAGISK_ALT_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json"; public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo"; public static final String MAGISK_ALT_REPO_JSDELIVR = "https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json"; - public static final String ANDROIDACY_MAGISK_REPO_ENDPOINT = "https://production-api.androidacy.com/magisk/repo"; public static final String ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT = "https://staging-api.androidacy.com/magisk/repo"; public static final String ANDROIDACY_MAGISK_REPO_HOMEPAGE = "https://www.androidacy.com/modules-repo"; - public static final String DG_MAGISK_REPO = "https://repo.dergoogler.com/modules.json"; public static final String DG_MAGISK_REPO_GITHUB = "https://googlers-magisk-repo.github.io/modules.json"; public static final String DG_MAGISK_REPO_GITHUB_RAW = "https://raw.githubusercontent.com/Googlers-Repo/googlers-repo.github.io/master/modules.json"; - + private static final String TAG = "RepoManager"; + private static final String MAGISK_REPO_MANAGER = + "https://magisk-modules-repo.github.io/submission/modules.json"; private static final Object lock = new Object(); + private static final double STEP1 = 0.1D; + private static final double STEP2 = 0.8D; + private static final double STEP3 = 0.1D; private static volatile RepoManager INSTANCE; + private final MainApplication mainApplication; + private final LinkedHashMap repoData; + private final HashMap modules; + private final AndroidacyRepoData androidacyRepoData; + private final CustomRepoManager customRepoManager; + private boolean hasInternet; + private boolean repoLastError = false; + private boolean initialized; + public String repoLastErrorName = null; + + private RepoManager(MainApplication mainApplication) { + INSTANCE = this; // Set early fox XHooks + this.initialized = false; + this.mainApplication = mainApplication; + this.repoData = new LinkedHashMap<>(); + this.modules = new HashMap<>(); + // We do not have repo list config yet. + RepoData altRepo = this.addRepoData( + MAGISK_ALT_REPO, "Magisk Modules Alt Repo"); + altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE; + altRepo.defaultSubmitModule = + "https://github.com/Magisk-Modules-Alt-Repo/submission/issues"; + RepoData dgRepo = this.addRepoData( + DG_MAGISK_REPO_GITHUB_RAW, "Googlers Magisk Repo"); + dgRepo.defaultWebsite = "https://dergoogler.com/repo"; + this.androidacyRepoData = this.addAndroidacyRepoData(); + this.customRepoManager = new CustomRepoManager(mainApplication, this); + XHooks.onRepoManagerInitialize(); + // Populate default cache + boolean x = false; + for (RepoData repoData : this.repoData.values()) { + if (repoData == this.androidacyRepoData) { + if (x) return; + x = true; + } + this.populateDefaultCache(repoData); + } + this.initialized = true; + } public static RepoManager getINSTANCE() { if (INSTANCE == null || !INSTANCE.initialized) { @@ -93,44 +133,50 @@ public static RepoManager getINSTANCE_UNSAFE() { return INSTANCE; } - private final MainApplication mainApplication; - private final LinkedHashMap repoData; - private final HashMap modules; - private final AndroidacyRepoData androidacyRepoData; - private final CustomRepoManager customRepoManager; - private boolean initialized; + public static String internalIdOfUrl(String url) { + switch (url) { + case MAGISK_ALT_REPO: + case MAGISK_ALT_REPO_JSDELIVR: + return "magisk_alt_repo"; + case ANDROIDACY_MAGISK_REPO_ENDPOINT: + case ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT: + return "androidacy_repo"; + case DG_MAGISK_REPO: + case DG_MAGISK_REPO_GITHUB: + case DG_MAGISK_REPO_GITHUB_RAW: + return "dg_magisk_repo"; + default: + return "repo_" + Hashes.hashSha1( + url.getBytes(StandardCharsets.UTF_8)); + } + } - private RepoManager(MainApplication mainApplication) { - INSTANCE = this; // Set early fox XHooks - this.initialized = false; - this.mainApplication = mainApplication; - this.repoData = new LinkedHashMap<>(); - this.modules = new HashMap<>(); - // We do not have repo list config yet. - RepoData altRepo = this.addRepoData( - MAGISK_ALT_REPO, "Magisk Modules Alt Repo"); - altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE; - altRepo.defaultSubmitModule = - "https://github.com/Magisk-Modules-Alt-Repo/submission/issues"; - RepoData dgRepo = this.addRepoData( - DG_MAGISK_REPO_GITHUB_RAW, "Googlers Magisk Repo"); - dgRepo.defaultWebsite = "https://dergoogler.com/repo"; - this.androidacyRepoData = this.addAndroidacyRepoData(); - this.customRepoManager = new CustomRepoManager(mainApplication, this); - XHooks.onRepoManagerInitialize(); - // Populate default cache - boolean x = false; - for (RepoData repoData:this.repoData.values()) { - if (repoData == this.androidacyRepoData) { - if (x) return; x = true; - } - this.populateDefaultCache(repoData); + static boolean isBuiltInRepo(String repo) { + switch (repo) { + case RepoManager.MAGISK_ALT_REPO: + case RepoManager.MAGISK_ALT_REPO_JSDELIVR: + case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT: + case RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT: + case RepoManager.DG_MAGISK_REPO: + case RepoManager.DG_MAGISK_REPO_GITHUB: + case RepoManager.DG_MAGISK_REPO_GITHUB_RAW: + return true; } - this.initialized = true; + return false; + } + + /** + * Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()} + * without initializing RepoManager + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isAndroidacyRepoEnabled() { + return INSTANCE != null && INSTANCE.androidacyRepoData != null && + INSTANCE.androidacyRepoData.isEnabled(); } private void populateDefaultCache(RepoData repoData) { - for (RepoModule repoModule:repoData.moduleHashMap.values()) { + for (RepoModule repoModule : repoData.moduleHashMap.values()) { if (!repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) { RepoModule registeredRepoModule = this.modules.get(repoModule.id); if (registeredRepoModule == null) { @@ -181,14 +227,9 @@ public RepoData addOrGet(String url, String fallBackName) { return repoData; } - private boolean repoLastResult = true; - - private static final double STEP1 = 0.1D; - private static final double STEP2 = 0.8D; - private static final double STEP3 = 0.1D; - 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); @@ -215,7 +256,7 @@ protected void scanInternal(@NonNull UpdateListener updateListener) { noodleDebug.replace(repoData.getName()); Log.d(TAG, "Registering " + repoData.getName()); noodleDebug.push(""); - for (RepoModule repoModule:repoModules) { + for (RepoModule repoModule : repoModules) { noodleDebug.replace(repoModule.id); try { if (repoModule.propUrl != null && @@ -243,7 +284,7 @@ protected void scanInternal(@NonNull UpdateListener updateListener) { updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules)); } noodleDebug.pop(); - for (RepoModule repoModule:repoUpdaters[i].toApply()) { + for (RepoModule repoModule : repoUpdaters[i].toApply()) { if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) { RepoModule registeredRepoModule = this.modules.get(repoModule.id); if (registeredRepoModule == null) { @@ -258,21 +299,45 @@ protected void scanInternal(@NonNull UpdateListener updateListener) { noodleDebug.pop(); noodleDebug.replace("Finishing update"); noodleDebug.push(""); - boolean hasInternet = false; - for (int i = 0; i < repoDatas.length; i++) { - noodleDebug.replace(repoUpdaters[i].repoData.getName()); - hasInternet |= repoUpdaters[i].finish(); - updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1))); + this.hasInternet = false; + // Check if we have internet connection + // Attempt to contact connectivitycheck.gstatic.com/generate_204 + // If we can't, we don't have internet connection + try { + HttpURLConnection urlConnection = (HttpURLConnection) new URL( + "https://connectivitycheck.gstatic.com/generate_204").openConnection(); + urlConnection.setInstanceFollowRedirects(false); + urlConnection.setConnectTimeout(1000); + urlConnection.setReadTimeout(1000); + urlConnection.setUseCaches(false); + urlConnection.getInputStream().close(); + if (urlConnection.getResponseCode() == 204 && + urlConnection.getContentLength() == 0) { + this.hasInternet = true; + } + } catch (IOException e) { + Log.e(TAG, "Failed to check internet connection", e); + } + noodleDebug.pop(); + if (hasInternet) { + for (int i = 0; i < repoDatas.length; i++) { + noodleDebug.replace(repoUpdaters[i].repoData.getName()); + this.repoLastError = !repoUpdaters[i].finish(); + if (this.repoLastError) { + Log.e(TAG, "Failed to update " + repoUpdaters[i].repoData.getName()); + this.repoLastErrorName = repoUpdaters[i].repoData.getName(); + } + updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1))); + } } noodleDebug.pop(); Log.i(TAG, "Got " + this.modules.size() + " modules!"); updateListener.update(1D); - this.repoLastResult = hasInternet; noodleDebug.pop(); // pop "Finishing update" } public void updateEnabledStates() { - for (RepoData repoData:this.repoData.values()) { + for (RepoData repoData : this.repoData.values()) { boolean wasEnabled = repoData.isEnabled(); repoData.updateEnabledState(); if (!wasEnabled && repoData.isEnabled()) { @@ -287,39 +352,7 @@ public HashMap getModules() { } public boolean hasConnectivity() { - return this.repoLastResult; - } - - public static String internalIdOfUrl(String url) { - switch (url) { - case MAGISK_ALT_REPO: - case MAGISK_ALT_REPO_JSDELIVR: - return "magisk_alt_repo"; - case ANDROIDACY_MAGISK_REPO_ENDPOINT: - case ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT: - return "androidacy_repo"; - case DG_MAGISK_REPO: - case DG_MAGISK_REPO_GITHUB: - case DG_MAGISK_REPO_GITHUB_RAW: - return "dg_magisk_repo"; - default: - return "repo_" + Hashes.hashSha1( - url.getBytes(StandardCharsets.UTF_8)); - } - } - - static boolean isBuiltInRepo(String repo) { - switch (repo) { - case RepoManager.MAGISK_ALT_REPO: - case RepoManager.MAGISK_ALT_REPO_JSDELIVR: - case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT: - case RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT: - case RepoManager.DG_MAGISK_REPO: - case RepoManager.DG_MAGISK_REPO_GITHUB: - case RepoManager.DG_MAGISK_REPO_GITHUB_RAW: - return true; - } - return false; + return this.hasInternet; } private RepoData addRepoData(String url, String fallBackName) { @@ -374,12 +407,7 @@ public Collection getXRepos() { return new LinkedHashSet<>(this.repoData.values()); } - /** - * Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()} - * without initializing RepoManager - */ - public static boolean isAndroidacyRepoEnabled() { - return INSTANCE != null && INSTANCE.androidacyRepoData != null && - INSTANCE.androidacyRepoData.isEnabled(); + public boolean isLastUpdateSuccess() { + return this.repoLastError; } } 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..c7145ace0 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -1,14 +1,25 @@ 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; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -40,6 +51,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 (Arrays.hashCode(this.indexRaw) == 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 @@ -66,6 +84,10 @@ public Collection toApply() { public boolean finish() { final boolean success = this.indexRaw != null; + // If repo is not enabled we don't need to do anything, just return true + if (!this.repoData.isEnabled()) { + return true; + } if (this.indexRaw != null) { try { Files.write(this.repoData.metaDataCache, this.indexRaw); 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 f959e1a1c..446671794 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; @@ -65,6 +67,7 @@ import org.json.JSONException; import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.Objects; import java.util.Random; @@ -262,12 +265,39 @@ 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()) { + // 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.setOnPreferenceChangeListener((preference, newValue) -> { boolean enabled = Boolean.parseBoolean(String.valueOf(newValue)); debugNotification.setEnabled(enabled); @@ -422,6 +452,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); @@ -497,17 +529,44 @@ public void onCreatePreferencesAndroidacy() { return true; }); } - 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"); - assert prefAndroidacyRepoApiKey != null; + // Disable toggling the pref_androidacy_repo_enabled on builds without an + // ANDROIDACY_CLIENT_ID or where the ANDROIDACY_CLIENT_ID is empty + Preference androidacyRepoEnabled = Objects.requireNonNull(findPreference("pref_androidacy_repo_enabled")); + if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { + androidacyRepoEnabled.setOnPreferenceClickListener(preference -> { + new MaterialAlertDialogBuilder(this.requireContext()) + .setTitle(R.string.androidacy_repo_disabled) + .setMessage(R.string.androidacy_repo_disabled_message) + .setPositiveButton(R.string.download_full_app, (dialog, which) -> { + // User clicked OK button. Open GitHub releases page + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse( + "https://github.com/Fox2Code/FoxMagiskModuleManager/releases")); + startActivity(browserIntent); + }) + .show(); + // Revert the switch to off + SwitchPreferenceCompat switchPreferenceCompat = (SwitchPreferenceCompat) androidacyRepoEnabled; + switchPreferenceCompat.setChecked(false); + // Save the preference + MainApplication.getSharedPreferences().edit().putBoolean("pref_androidacy_repo_enabled", false).apply(); + return false; + }); + } + String[] originalApiKeyRef = new String[]{MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).getString("pref_androidacy_api_token", null)}; + // Get the dummy pref_androidacy_repo_api_token EditTextPreference + EditTextPreference prefAndroidacyRepoApiKey = Objects.requireNonNull(findPreference("pref_androidacy_api_token")); + prefAndroidacyRepoApiKey.setTitle(R.string.api_key); + prefAndroidacyRepoApiKey.setSummary(R.string.api_key_summary); + prefAndroidacyRepoApiKey.setDialogTitle(R.string.api_key); + prefAndroidacyRepoApiKey.setDefaultValue(originalApiKeyRef[0]); + // Set the value to the current value + prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]); prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> { editText.setSingleLine(); // Make the single line wrap editText.setHorizontallyScrolling(false); - // Set the height to the height of 2 lines - editText.setHeight(editText.getLineHeight() * 3); + // Set the height to the maximum required to fit the text + editText.setMaxLines(Integer.MAX_VALUE); // Make ok button say "Save" editText.setImeOptions(EditorInfo.IME_ACTION_DONE); }); @@ -528,41 +587,98 @@ public void onCreatePreferencesAndroidacy() { new Thread(() -> { // 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()); + MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().remove("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) { new Handler(Looper.getMainLooper()).post(() -> { Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show(); // Save the original key - MainApplication.getSharedPreferences().edit().putString( - "pref_androidacy_api_token", originalApiKeyRef[0]).apply(); + MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply(); // Re-show the dialog with an error prefAndroidacyRepoApiKey.performClick(); // Show error 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); - } catch (IOException ignored) {} + } catch (IOException | NoSuchAlgorithmException ignored) {} // If the key is valid, save it if (valid) { 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()); + MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("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(); // Save the original key - MainApplication.getSharedPreferences().edit().putString( + MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString( "pref_androidacy_api_token", originalApiKeyRef[0]).apply(); // Re-show the dialog with an error prefAndroidacyRepoApiKey.performClick(); @@ -575,6 +691,8 @@ public void onCreatePreferencesAndroidacy() { }).start(); return true; }); + // make sure the preference is visible if repo is enabled + prefAndroidacyRepoApiKey.setVisible(RepoManager.getINSTANCE().getAndroidacyRepoData().isEnabled()); } @SuppressLint("RestrictedApi") @@ -589,7 +707,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; @@ -615,13 +733,12 @@ 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() { try { customRepoData.quickPrePopulate(); - } catch (IOException | JSONException e) { + } catch (IOException | JSONException | NoSuchAlgorithmException e) { Log.e(TAG, "Failed to preload repo values", e); } UiThreadHandler.handler.post(() -> updateCustomRepoList(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 842efc82b..5be318733 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -1,5 +1,6 @@ package com.fox2code.mmm.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; @@ -18,6 +19,10 @@ import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.repo.RepoManager; +import com.google.net.cronet.okhttptransport.CronetCallFactory; +import com.google.net.cronet.okhttptransport.CronetInterceptor; + +import org.chromium.net.CronetEngine; import java.io.ByteArrayOutputStream; import java.io.File; @@ -48,7 +53,6 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; -import okhttp3.brotli.BrotliInterceptor; import okhttp3.dnsoverhttps.DnsOverHttps; import okio.BufferedSink; @@ -58,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; @@ -97,20 +98,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 +110,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 +127,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,20 +139,48 @@ public class Http { } return chain.proceed(request.build()); }); + // Add cronet interceptor + // install cronet + /*try { + // Detect if cronet is installed + CronetProviderInstaller.installProvider(mainApplication); + } catch (Exception e) { + Log.e(TAG, "Failed to install cronet", e); + }*/ + // init cronet + try { + // Load the cronet library + System.loadLibrary("cronet.108.0.5359.95"); + CronetEngine.Builder builder = new CronetEngine.Builder(mainApplication); + builder.enableBrotli(true); + builder.enableHttp2(true); + builder.enableQuic(true); + // Cache size is 10MB + // Make the directory if it does not exist + File cacheDir = new File(mainApplication.getCacheDir(), "cronet"); + if (!cacheDir.exists()) { + if (!cacheDir.mkdirs()) { + throw new IOException("Failed to create cronet cache directory"); + } + } + builder.setStoragePath(mainApplication.getCacheDir().getAbsolutePath() + "/cronet"); + builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 10 * 1024 * 1024); + 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)); + 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(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(); @@ -185,10 +198,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; } @@ -212,7 +221,7 @@ private static void checkNeedBlockAndroidacyRequest(String url) throws IOExcepti public static boolean needCaptchaAndroidacy() { return needCaptchaAndroidacyHost != null; } - + public static String needCaptchaAndroidacyHost() { return needCaptchaAndroidacyHost; } @@ -221,15 +230,19 @@ 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 + if (response.code() == 401 && AndroidacyUtil.isAndroidacyLink(url)) { + throw new HttpException("Androidacy token is invalid", 401); + } throw new HttpException(response.code()); } ResponseBody responseBody = response.body(); @@ -242,23 +255,18 @@ 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 - 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()); } @@ -320,61 +328,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); } @@ -462,17 +424,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/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-cs/arrays.xml b/app/src/main/res/values-cs/arrays.xml index cdd0aee22..e425a5415 100644 --- a/app/src/main/res/values-cs/arrays.xml +++ b/app/src/main/res/values-cs/arrays.xml @@ -1,14 +1,8 @@ - - Dle systému Tmavá + AMOLED Black Světlá diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml index 7ec366cf5..3df0c2941 100644 --- a/app/src/main/res/values-de/arrays.xml +++ b/app/src/main/res/values-de/arrays.xml @@ -3,6 +3,7 @@ Systemvorgabe Dunkel + AMOLED Black Hell diff --git a/app/src/main/res/values-el/arrays.xml b/app/src/main/res/values-el/arrays.xml index cf8bf649d..d95361099 100644 --- a/app/src/main/res/values-el/arrays.xml +++ b/app/src/main/res/values-el/arrays.xml @@ -2,6 +2,7 @@ Προεπιλογή συστήματως Σκωτεινό + AMOLED Black Ανοιχτό \ No newline at end of file diff --git a/app/src/main/res/values-es-rMX/arrays.xml b/app/src/main/res/values-es-rMX/arrays.xml index ba414a573..08b4fe0ad 100644 --- a/app/src/main/res/values-es-rMX/arrays.xml +++ b/app/src/main/res/values-es-rMX/arrays.xml @@ -2,6 +2,7 @@ Sistema Oscuro + AMOLED Black Claro \ No newline at end of file diff --git a/app/src/main/res/values-et/arrays.xml b/app/src/main/res/values-et/arrays.xml index 435cbe52f..e27dae93d 100644 --- a/app/src/main/res/values-et/arrays.xml +++ b/app/src/main/res/values-et/arrays.xml @@ -2,6 +2,7 @@ Süsteem Tume + AMOLED Black Hele diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml index 553d06add..f9319ba75 100644 --- a/app/src/main/res/values-fr/arrays.xml +++ b/app/src/main/res/values-fr/arrays.xml @@ -2,6 +2,7 @@ Système Sombre + AMOLED Black Clair diff --git a/app/src/main/res/values-id/arrays.xml b/app/src/main/res/values-id/arrays.xml index d1e948883..e9685f6ae 100644 --- a/app/src/main/res/values-id/arrays.xml +++ b/app/src/main/res/values-id/arrays.xml @@ -2,6 +2,7 @@ Sistem Gelap + AMOLED Black Terang diff --git a/app/src/main/res/values-it/arrays.xml b/app/src/main/res/values-it/arrays.xml index 8bf21641a..d43a2c247 100644 --- a/app/src/main/res/values-it/arrays.xml +++ b/app/src/main/res/values-it/arrays.xml @@ -2,6 +2,7 @@ Sistema Scuro + AMOLED Black Chiaro \ No newline at end of file diff --git a/app/src/main/res/values-ja/arrays.xml b/app/src/main/res/values-ja/arrays.xml index 9ec5b41ca..fa1604429 100644 --- a/app/src/main/res/values-ja/arrays.xml +++ b/app/src/main/res/values-ja/arrays.xml @@ -2,6 +2,7 @@ システムの設定を使用 ダーク + AMOLED Black ライト diff --git a/app/src/main/res/values-pl/arrays.xml b/app/src/main/res/values-pl/arrays.xml index 433db947d..f97f095ca 100644 --- a/app/src/main/res/values-pl/arrays.xml +++ b/app/src/main/res/values-pl/arrays.xml @@ -2,6 +2,7 @@ Zgodny z systemem Ciemny + AMOLED Black Jasny diff --git a/app/src/main/res/values-pt-rBR/arrays.xml b/app/src/main/res/values-pt-rBR/arrays.xml index 74401f898..9478cc2d1 100644 --- a/app/src/main/res/values-pt-rBR/arrays.xml +++ b/app/src/main/res/values-pt-rBR/arrays.xml @@ -2,6 +2,7 @@ Sistema Escuro + AMOLED Black Claro \ No newline at end of file diff --git a/app/src/main/res/values-ro/arrays.xml b/app/src/main/res/values-ro/arrays.xml index a1020864e..d8b9b1bb1 100644 --- a/app/src/main/res/values-ro/arrays.xml +++ b/app/src/main/res/values-ro/arrays.xml @@ -4,6 +4,7 @@ Sistem Întunecată + AMOLED Black Luminoasă diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml index 00d01517b..7829b4eb5 100644 --- a/app/src/main/res/values-ru/arrays.xml +++ b/app/src/main/res/values-ru/arrays.xml @@ -2,6 +2,7 @@ Как в системе Тёмная + AMOLED Black Светлая diff --git a/app/src/main/res/values-sk/arrays.xml b/app/src/main/res/values-sk/arrays.xml index 3aef1a716..e32d923d9 100644 --- a/app/src/main/res/values-sk/arrays.xml +++ b/app/src/main/res/values-sk/arrays.xml @@ -1,14 +1,8 @@ - - Podľa systému Tmavá + AMOLED Black Svetlá diff --git a/app/src/main/res/values-tr/arrays.xml b/app/src/main/res/values-tr/arrays.xml index fa85b7807..8ba56281e 100644 --- a/app/src/main/res/values-tr/arrays.xml +++ b/app/src/main/res/values-tr/arrays.xml @@ -2,6 +2,7 @@ Sistem Koyu + AMOLED Black Açık \ No newline at end of file diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index b70691e2f..3476aecfa 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -22,6 +22,19 @@ @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..24a86a813 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -83,6 +83,19 @@ @style/Widget.Material.Chip.Choice.Dark + + +