diff --git a/.github/workflows/build-aliucord.yml b/.github/workflows/build-aliucord.yml deleted file mode 100644 index 3a3626ffa..000000000 --- a/.github/workflows/build-aliucord.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Build Aliucord - -on: - push: - branches: - - main - paths: - - .github/workflows/build-aliucord.yml - - Aliucord/** - - .assets/data.json - -jobs: - build-aliucord: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@master - with: - path: src - - - name: Checkout builds - uses: actions/checkout@master - with: - ref: builds - path: builds - - - name: Setup JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Setup Android SDK - uses: android-actions/setup-android@3404b45d4c43e74e30dcad1a47fb89a0573f5a7e # v2.0.6 - - - name: Build Aliucord - run: | - cd $GITHUB_WORKSPACE/src - chmod +x gradlew - ./gradlew :Aliucord:make - cp Aliucord/build/Aliucord.zip $GITHUB_WORKSPACE/builds - env: - MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - - - name: Push builds - run: | - cd $GITHUB_WORKSPACE/builds - cp ../src/.assets/AndroidManifest.xml . - if git diff --exit-code Aliucord.zip >/dev/null; then - # No changes to Aliucord.zip, simply merge builds data.json and Aliucord data.json to keep the old aliucordHash - jq -s '.[0] * .[1]' data.json ../src/.assets/data.json > data.json.new - mv data.json.new data.json - else - # Changes to Aliucord.zip, update aliucordHash - jq ". + { aliucordHash: \"$(git --git-dir=../src/.git rev-parse --short HEAD)\" }" < ../src/.assets/data.json > data.json - fi - git config --local user.email "actions@github.com" - git config --local user.name "GitHub Actions" - git pull - git add . - git commit -m "Build $GITHUB_SHA" - git push diff --git a/.github/workflows/build-injector.yml b/.github/workflows/build-injector.yml deleted file mode 100644 index 34d92ad5e..000000000 --- a/.github/workflows/build-injector.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build Injector - -on: - push: - branches: - - main - paths: - - .github/workflows/build-injector.yml - - Injector/** - -jobs: - build-injector: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@master - with: - path: src - - - name: Checkout builds - uses: actions/checkout@master - with: - ref: builds - path: builds - - - name: Setup JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Setup Android SDK - uses: android-actions/setup-android@3404b45d4c43e74e30dcad1a47fb89a0573f5a7e # v2.0.6 - - - name: Build Injector - run: | - cd $GITHUB_WORKSPACE/src - chmod +x gradlew - ./gradlew :Injector:make - cp Injector/build/Injector.dex $GITHUB_WORKSPACE/builds - - - name: Push builds - run: | - cd $GITHUB_WORKSPACE/builds - git config --local user.email "actions@github.com" - git config --local user.name "GitHub Actions" - git pull - git add . - git commit -m "Build $GITHUB_SHA" - git push diff --git a/.github/workflows/build-installer.yml b/.github/workflows/build-installer.yml index 49c77fdf7..aabababc9 100644 --- a/.github/workflows/build-installer.yml +++ b/.github/workflows/build-installer.yml @@ -23,10 +23,16 @@ jobs: with: path: src + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup Flutter - uses: subosito/flutter-action@48cafc24713cca54bbe03cdc3a423187d413aafa # v2.10.0 + uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # v2.16.0 with: - flutter-version: '3.10.6' + flutter-version: '3.24.0' - name: Build Installer run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..e05721b5c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,151 @@ +name: Build + +on: + push: + paths-ignore: + - "**.md" + pull_request: + paths-ignore: + - "**.md" + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 8 + # Prevent duplicate workflow run for PRs from same repo + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork }} + permissions: + contents: read + steps: + - name: Checkout src + uses: actions/checkout@v4 + with: + path: src + persist-credentials: false + + - name: Setup JDK 11 + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "11" + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + cmdline-tools-version: 9862592 # 10.0 is the last version working with JDK 11 + + - name: Build project + run: | + cd $GITHUB_WORKSPACE/src + + # Check if this should be marked as a release build + export RELEASE=${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} + + chmod +x gradlew + ./gradlew --stacktrace :Aliucord:make :Injector:make :patches:package :patches:disassembleWithPatches :patches:test + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build + if-no-files-found: error + path: | + ${{ github.workspace }}/src/Aliucord/build/Aliucord.zip + ${{ github.workspace }}/src/Injector/build/Injector.dex + ${{ github.workspace }}/src/patches/build/patches.zip + + deploy: + runs-on: ubuntu-24.04 + timeout-minutes: 3 + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + needs: [ build ] + permissions: + contents: write + steps: + - name: Checkout src + uses: actions/checkout@v4 + with: + path: src + + - name: Checkout builds + uses: actions/checkout@v4 + with: + ref: builds + path: builds + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build + path: artifacts + + - name: Deploy builds + if: github.ref == 'refs/heads/main' + run: | + # Flatten downloaded artifacts + find $GITHUB_WORKSPACE/artifacts -type f -exec mv -t $GITHUB_WORKSPACE/artifacts '{}' + + + # Legacy Installer support for now + cp $GITHUB_WORKSPACE/src/.assets/AndroidManifest.xml $GITHUB_WORKSPACE/builds + + # Extract component versions + coreVersion=$(cat $GITHUB_WORKSPACE/src/Aliucord/build.gradle.kts | grep -E 'version = "' | cut -d \" -f 2) + patchesVersion=$(cat $GITHUB_WORKSPACE/src/patches/build.gradle.kts | grep -E 'version = "' | cut -d \" -f 2) + injectorVersion=$(cat $GITHUB_WORKSPACE/src/Injector/build.gradle.kts | grep -E 'version = "' | cut -d \" -f 2) + + # Copy over builds if version changed + cd $GITHUB_WORKSPACE/builds + [ "$(jq -r '.coreVersion' data.json)" != "$coreVersion" ] && cp $GITHUB_WORKSPACE/artifacts/Aliucord.zip . + [ "$(jq -r '.injectorVersion' data.json)" != "$injectorVersion" ] && cp $GITHUB_WORKSPACE/artifacts/Injector.dex . + [ "$(jq -r '.patchesVersion' data.json)" != "$patchesVersion" ] && cp $GITHUB_WORKSPACE/artifacts/patches.zip . + + # Write versions to data.json + # `aliucordHash` is kept to force old builds to update + jq '. + { coreVersion: $cv, injectorVersion: $iv, patchesVersion: $pv, aliucordHash: "0000000" }' \ + --arg cv $coreVersion \ + --arg iv $injectorVersion \ + --arg pv $patchesVersion \ + ../src/.assets/data.json > data.json + + git config --local user.email "actions@github.com" + git config --local user.name "GitHub Actions" + git add . + if [[ `git status --porcelain` ]]; then + git commit -m "Build $GITHUB_SHA" + git push + fi + + # Publish core to maven if not originating from a PR + maven: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + if: github.event_name != 'pull_request' + needs: [ build ] + permissions: + contents: read + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + steps: + - name: Checkout src + if: ${{ env.MAVEN_USERNAME != '' }} + uses: actions/checkout@v4 + with: + path: src + persist-credentials: false + + - name: Setup JDK 11 + if: ${{ env.MAVEN_USERNAME != '' }} + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "11" + + - name: Build & Publish to Maven + if: ${{ env.MAVEN_USERNAME != '' }} + run: | + cd $GITHUB_WORKSPACE/src + chmod +x gradlew + ./gradlew :Aliucord:publish --stacktrace -Pversion=$GITHUB_REF_NAME-SNAPSHOT + ./gradlew :Aliucord:publish --stacktrace -Pversion=$(git rev-parse --short "$GITHUB_SHA") | exit 0 + env: + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} diff --git a/.github/workflows/publish-aliucord.yml b/.github/workflows/publish-aliucord.yml deleted file mode 100644 index bb8df6db9..000000000 --- a/.github/workflows/publish-aliucord.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Publish Aliucord - -on: - push: - paths: - - .github/workflows/publish-aliucord.yml - - Aliucord/** - -jobs: - build-aliucord: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@master - - - name: Setup JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Build Aliucord - run: | - chmod +x gradlew - ./gradlew :Aliucord:publish -Pversion=${GITHUB_REF##*/}-SNAPSHOT - ./gradlew :Aliucord:publish -Pversion=$(git rev-parse --short "$GITHUB_SHA") | exit 0 - env: - MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} diff --git a/.github/workflows/upload-pr-artifact.yml b/.github/workflows/upload-pr-artifact.yml deleted file mode 100644 index c4a5b1df5..000000000 --- a/.github/workflows/upload-pr-artifact.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: PR Artifact - -on: - pull_request: - paths: - - .github/workflows/upload-pr-artifact.yml - - Aliucord/** - - gradle/** - - settings.gradle - - gradlew - - gradle.properties - - build.gradle - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Setup JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Setup Android SDK - uses: android-actions/setup-android@3404b45d4c43e74e30dcad1a47fb89a0573f5a7e # v2.0.6 - - - name: Build Aliucord - run: | - cd $GITHUB_WORKSPACE - chmod +x gradlew - ./gradlew :Aliucord:make - - - name: Upload artifact - uses: actions/upload-artifact@v2 - with: - name: Aliucord - path: Aliucord/build/Aliucord.zip diff --git a/.gitignore b/.gitignore index bdceae326..a4c4c7edf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ local.properties *.classpath *.project *org.eclipse.buildship.core.prefs -.vscode \ No newline at end of file +.vscode +/patches/smali/ diff --git a/Aliucord/build.gradle.kts b/Aliucord/build.gradle.kts index 442e3b020..5c62ad449 100644 --- a/Aliucord/build.gradle.kts +++ b/Aliucord/build.gradle.kts @@ -5,17 +5,8 @@ plugins { id("org.jetbrains.dokka") } -fun getGitHash(): String { - val stdout = org.apache.commons.io.output.ByteArrayOutputStream() - exec { - commandLine = listOf("git", "rev-parse", "--short", "HEAD") - standardOutput = stdout - isIgnoreExitValue = true - } - return stdout.toString().trim() -} - group = "com.aliucord" +version = "2.0.1" aliucord { projectType.set(com.aliucord.gradle.ProjectType.CORE) @@ -23,7 +14,8 @@ aliucord { android { defaultConfig { - buildConfigField("String", "GIT_REVISION", "\"${getGitHash()}\"") + buildConfigField("String", "VERSION", "\"$version\"") + buildConfigField("boolean", "RELEASE", System.getenv("RELEASE") ?: "false") buildConfigField("int", "DISCORD_VERSION", libs.versions.discord.get()) } diff --git a/Aliucord/src/main/java/com/aliucord/Main.java b/Aliucord/src/main/java/com/aliucord/Main.java index 4623bc165..a1ccfe1c5 100644 --- a/Aliucord/src/main/java/com/aliucord/Main.java +++ b/Aliucord/src/main/java/com/aliucord/Main.java @@ -9,13 +9,15 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Typeface; +import android.net.Uri; import android.os.*; +import android.provider.Settings; import android.text.*; import android.text.style.AbsoluteSizeSpan; -import android.view.View; -import android.view.ViewGroup; +import android.view.*; import android.widget.*; import androidx.activity.result.contract.ActivityResultContracts; @@ -25,7 +27,7 @@ import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; -import com.aliucord.coreplugins.CorePlugins; +import com.aliucord.entities.CorePlugin; import com.aliucord.entities.Plugin; import com.aliucord.patcher.*; import com.aliucord.settings.*; @@ -41,11 +43,11 @@ import com.discord.databinding.WidgetDebuggingAdapterItemBinding; import com.discord.models.domain.emoji.ModelEmojiUnicode; import com.discord.stores.StoreStream; -import com.discord.stores.StoreExperiments; import com.discord.utilities.color.ColorCompat; import com.discord.utilities.guildautomod.AutoModUtils; import com.discord.utilities.user.UserUtils; import com.discord.widgets.changelog.WidgetChangeLog; +import com.discord.widgets.chat.input.SmoothKeyboardReactionHelper; import com.discord.widgets.chat.list.WidgetChatList; import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemAutoModSystemMessageEmbed; import com.discord.widgets.chat.list.entries.AutoModSystemMessageEmbedEntry; @@ -63,6 +65,7 @@ import java.util.*; import dalvik.system.PathClassLoader; +import kotlin.io.FilesKt; public final class Main { /** Whether Aliucord has been preInitialized */ @@ -96,7 +99,7 @@ public static void preInit(AppActivity activity) throws NoSuchMethodException { private static void preInitWithPermissions(AppCompatActivity activity) { settings = new SettingsUtilsJSON("Aliucord"); - CorePlugins.loadAll(activity); + PluginManager.loadCorePlugins(activity); loadAllPlugins(activity); } @@ -146,7 +149,8 @@ public static void init(AppActivity activity) { ); TextView versionView = layout.findViewById(Utils.getResId("app_info_header", "id")); - var text = versionView.getText() + " | Aliucord " + BuildConfig.GIT_REVISION; + var text = versionView.getText() + " | Aliucord " + BuildConfig.VERSION; + if (!BuildConfig.RELEASE) text += " (Custom)"; if (Utils.isDebuggable()) text += " [DEBUGGABLE]"; versionView.setText(text); @@ -249,19 +253,6 @@ public static void init(AppActivity activity) { Thread.setDefaultUncaughtExceptionHandler(Main::crashHandler); - // set default overrides for experiments - var experiments = StoreStream.getExperiments(); - var overrides = StoreExperiments.access$getExperimentOverrides$p(experiments); - for (var key : new String[]{ - "2020-09_threads", - "2021-02_view_threads", - "2021-08_threads_permissions", - "2021-10_android_attachment_bottom_sheet", - "2021-11_guild_communication_disabled_users", - "2021-11_guild_communication_disabled_guilds", - "2022-03_text_in_voice" - }) if (!overrides.containsKey(key)) experiments.setOverride(key, 1); - // use new member profile editor for nitro users try { Patcher.addPatch( @@ -278,8 +269,27 @@ public static void init(AppActivity activity) { ); } catch (Throwable e) { logger.error(e); } + // not sure why this happens, reported on Android 15 Beta 4 + // java.lang.IllegalArgumentException: Animators cannot have negative duration: -1 + // at android.view.ViewPropertyAnimator.setDuration(ViewPropertyAnimator.java:266) + // at com.discord.widgets.chat.input.SmoothKeyboardReactionHelper$Callback.onStart(SmoothKeyboardReactionHelper.kt:5) + // at android.view.View.dispatchWindowInsetsAnimationStart(View.java:12671) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + try { + Patcher.addPatch( + SmoothKeyboardReactionHelper.Callback.class.getDeclaredMethod("onStart", WindowInsetsAnimation.class, WindowInsetsAnimation.Bounds.class), + new PreHook(param -> { + var animation = (WindowInsetsAnimation) param.args[0]; + if (animation.getDurationMillis() < 0) param.setResult(param.args[1]); + }) + ); + } catch (Throwable e) { + logger.error("Couldn't patch possible Android 15 (?) crash", e); + } + } + if (loadedPlugins) { - CorePlugins.startAll(activity); + PluginManager.startCorePlugins(); startAllPlugins(); } } @@ -392,7 +402,7 @@ private static void loadAllPlugins(Context context) { true ); } - rmrf(f); + FilesKt.deleteRecursively(f); } } @@ -402,17 +412,12 @@ private static void loadAllPlugins(Context context) { loadedPlugins = true; } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static void rmrf(File file) { - if (file.isDirectory()) { - for (var child : file.listFiles()) - rmrf(child); - } - file.delete(); - } - private static void startAllPlugins() { - for (String name : PluginManager.plugins.keySet()) { + for (Map.Entry entry : PluginManager.plugins.entrySet()) { + // coreplugins are started separately + if (entry.getValue() instanceof CorePlugin) continue; + + var name = entry.getKey(); try { if (PluginManager.isPluginEnabled(name)) PluginManager.startPlugin(name); @@ -425,16 +430,38 @@ private static void startAllPlugins() { Utils.threadPool.execute(() -> PluginUpdater.checkUpdates(true)); } + private static void permissionGrantedCallback(AppCompatActivity activity, boolean granted) { + if (granted) { + preInitWithPermissions(activity); + PluginManager.startCorePlugins(); + startAllPlugins(); + } else Toast.makeText(activity, "You have to grant storage permission to use Aliucord", Toast.LENGTH_LONG).show(); + } + private static boolean checkPermissions(AppCompatActivity activity) { - String perm = Manifest.permission.WRITE_EXTERNAL_STORAGE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageLegacy()) { + if (Environment.isExternalStorageManager()) return true; + Toast.makeText( + activity, + "Please grant all files permission, so Aliucord can access its folder in Internal Storage", + Toast.LENGTH_LONG + ).show(); + activity.registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> permissionGrantedCallback(activity, Environment.isExternalStorageManager()) + ).launch( + new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData(Uri.parse("package:" + activity.getPackageName())) + ); + return false; + } + + var perm = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (activity.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) return true; - activity.registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { - if (granted == Boolean.TRUE) { - preInitWithPermissions(activity); - CorePlugins.startAll(activity); - startAllPlugins(); - } else Toast.makeText(activity, "You have to grant storage permission to use Aliucord", Toast.LENGTH_LONG).show(); - }).launch(perm); + activity.registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> permissionGrantedCallback(activity, granted) + ).launch(perm); return false; } } diff --git a/Aliucord/src/main/java/com/aliucord/PluginManager.java b/Aliucord/src/main/java/com/aliucord/PluginManager.java index dfe82a855..53907810c 100644 --- a/Aliucord/src/main/java/com/aliucord/PluginManager.java +++ b/Aliucord/src/main/java/com/aliucord/PluginManager.java @@ -10,15 +10,19 @@ import android.content.res.AssetManager; import android.content.res.Resources; +import com.aliucord.coreplugins.badges.SupporterBadges; +import com.aliucord.coreplugins.plugindownloader.PluginDownloader; +import com.aliucord.coreplugins.slashcommandsfix.SlashCommandsFix; +import com.aliucord.coreplugins.rn.RNAPI; +import com.aliucord.entities.CorePlugin; import com.aliucord.entities.Plugin; import com.aliucord.patcher.Patcher; import com.aliucord.patcher.PreHook; -import com.aliucord.utils.GsonUtils; -import com.aliucord.utils.MapUtils; +import com.aliucord.coreplugins.*; +import com.aliucord.utils.*; import java.io.File; import java.io.InputStreamReader; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; @@ -33,17 +37,6 @@ public class PluginManager { /** Plugins that failed to load for various reasons. Map of file to String or Exception */ public static final Map failedToLoad = new LinkedHashMap<>(); - private static final Field manifestField; - - static { - try { - manifestField = Plugin.class.getDeclaredField("manifest"); - manifestField.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - /** * Loads a plugin * @@ -78,10 +71,9 @@ public static void loadPlugin(Context context, File file) { Patcher.addPatch(pluginClass.getDeclaredConstructor(), new PreHook(param -> { var plugin = (Plugin) param.thisObject; try { - manifestField.set(plugin, manifest); - } catch (IllegalAccessException e) { + ReflectUtils.setField(Plugin.class, plugin, "manifest", manifest); + } catch (Exception e) { logger.errorToast("Failed to set manifest for " + manifest.name); - logger.error(e); } })); @@ -116,6 +108,11 @@ public static void loadPlugin(Context context, File file) { public static void unloadPlugin(String name) { logger.info("Unloading plugin: " + name); var plugin = plugins.get(name); + + if (plugin instanceof CorePlugin) { + throw new IllegalArgumentException("Cannot unload coreplugin " + name); + } + if (plugin != null) try { plugin.unload(Utils.getAppContext()); plugins.remove(name); @@ -183,7 +180,13 @@ public static void startPlugin(String name) { public static void stopPlugin(String name) { logger.info("Stopping plugin: " + name); try { - Objects.requireNonNull(plugins.get(name)).stop(Utils.getAppContext()); + Plugin p = plugins.get(name); + + if (p instanceof CorePlugin && ((CorePlugin) p).isRequired()) { + throw new IllegalArgumentException("Cannot stop required coreplugin " + name); + } + + Objects.requireNonNull(p).stop(Utils.getAppContext()); } catch (Throwable e) { logger.error("Exception while stopping plugin " + name, e); } } @@ -218,6 +221,9 @@ public static String getPluginPrefKey(String name) { * @return Whether the plugin is enabled */ public static boolean isPluginEnabled(String name) { + Plugin p = plugins.get(name); + if (p instanceof CorePlugin && ((CorePlugin) p).isRequired()) return true; + return Main.settings.getBool(getPluginPrefKey(name), true); } @@ -231,4 +237,45 @@ public static boolean isPluginEnabled(String name) { public static boolean isPluginEnabled(Plugin plugin) { return isPluginEnabled(MapUtils.getMapKey(plugins, plugin)); } + + static void loadCorePlugins(Context context) { + CorePlugin[] corePlugins = { + new ButtonsAPI(), + new CommandHandler(), + new CoreCommands(), + new DefaultStickers(), + new ExperimentDefaults(), + new GifPreviewFix(), + new MembersListFix(), + new NoTrack(), + new PluginDownloader(), + new PrivateChannelsListScroll(), + new PrivateThreads(), + new RNAPI(), + new Pronouns(), + new SupportWarn(), + new SupporterBadges(), + new TokenLogin(), + new UploadSize(), + new SlashCommandsFix(), + }; + + for (Plugin p : corePlugins) { + logger.info("Loading coreplugin: " + p.getName()); + try { + plugins.put(p.getName(), p); + p.load(context); + } catch (Throwable e) { + logger.errorToast("Failed to load coreplugin " + p.getName(), e); + } + } + } + + static void startCorePlugins() { + for (Plugin p : plugins.values()) { + if (!(p instanceof CorePlugin)) continue; + if (!isPluginEnabled(p.getName())) continue; + startPlugin(p.getName()); + } + } } diff --git a/Aliucord/src/main/java/com/aliucord/Utils.kt b/Aliucord/src/main/java/com/aliucord/Utils.kt index 1ed109dc7..6f6756dbf 100644 --- a/Aliucord/src/main/java/com/aliucord/Utils.kt +++ b/Aliucord/src/main/java/com/aliucord/Utils.kt @@ -271,6 +271,7 @@ Consider installing the MiXplorer file manager, or navigate to $path manually us ) @JvmOverloads @JvmStatic + @Suppress("unused") fun showToast(ctx: Context, message: String, showLonger: Boolean = false) { showToast(message, showLonger) } diff --git a/Aliucord/src/main/java/com/aliucord/api/CommandsAPI.java b/Aliucord/src/main/java/com/aliucord/api/CommandsAPI.java index 41ba3a38f..79f5e8949 100644 --- a/Aliucord/src/main/java/com/aliucord/api/CommandsAPI.java +++ b/Aliucord/src/main/java/com/aliucord/api/CommandsAPI.java @@ -216,7 +216,7 @@ private static void _registerCommand( "Oops! Something went wrong while running this command:\n```java\n%s```\n" + "Please search for this error on the Aliucord server to see if it's a known issue. " + "If it isn't, report it to the plugin %s%s.\n\n" + - "Debug:```\nCommand: %s\nPlugin: %s v%s\nDiscord v%s\nAndroid %s (SDK %d)\nAliucord %s```\nArguments:```\n%s```\n", + "Debug:```\nCommand: %s\nPlugin: %s v%s\nDiscord v%s\nAndroid %s (SDK %d)\nAliucord %s %s```\nArguments:```\n%s```\n", t, manifest.authors.length == 1 ? "author" : "authors", manifest.authors.length != 0 ? " (" + TextUtils.join(", ", manifest.authors) + ")" : "", @@ -226,7 +226,8 @@ private static void _registerCommand( Constants.DISCORD_VERSION, Build.VERSION.RELEASE, Build.VERSION.SDK_INT, - BuildConfig.GIT_REVISION, + BuildConfig.VERSION, + BuildConfig.RELEASE ? "" : "(Custom)", argString.length() != 0 ? argString : "-" ); } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/Badges.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/Badges.kt deleted file mode 100644 index 8a68e2c6b..000000000 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/Badges.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * This file is part of Aliucord, an Android Discord client mod. - * Copyright (c) 2021 Juby210 & Vendicated - * Licensed under the Open Software License version 3.0 - */ - -package com.aliucord.coreplugins - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import com.aliucord.* -import com.aliucord.entities.Plugin -import com.aliucord.patcher.* -import com.aliucord.utils.DimenUtils.dp -import com.discord.databinding.UserProfileHeaderBadgeBinding -import com.discord.models.guild.Guild -import com.discord.utilities.views.SimpleRecyclerAdapter -import com.discord.widgets.channels.list.WidgetChannelsList -import com.discord.widgets.user.Badge -import com.discord.widgets.user.profile.UserProfileHeaderView -import com.discord.widgets.user.profile.UserProfileHeaderViewModel -import com.lytefast.flexinput.R -import java.util.concurrent.atomic.AtomicBoolean - -internal class Badges : Plugin(Manifest("Badges")) { - class CustomBadge(val id: String?, val url: String?, val text: String) { - fun getDrawableId() = if (id == null) 0 else try { - R.e::class.java.getDeclaredField(id).getInt(null) - } catch (e: Throwable) { - Main.logger.error("Failed to get drawable for $id", e) - 0 - } - - fun toDiscordBadge() = Badge(getDrawableId(), null, text, false, url) - } - class UserBadges(val roles: Array?, val custom: Array?) - - // TODO: move to single fetch from https://aliucord.com/files/badges/data.json - private val url = "https://aliucord.com/badges" - - private val userBadges = HashMap?>() - private val guildBadges = HashMap() - private val cache = HashMap() - - private val badgesAdapter = UserProfileHeaderView::class.java.getDeclaredField("badgesAdapter").apply { isAccessible = true } - private val dataField = SimpleRecyclerAdapter::class.java.getDeclaredField("data").apply { isAccessible = true } - - private val guildBadgeViewId = View.generateViewId() - - override fun load(context: Context) { - val fetchingBadges = AtomicBoolean(false) - Patcher.addPatch( - UserProfileHeaderView::class.java.getDeclaredMethod("updateViewState", UserProfileHeaderViewModel.ViewState.Loaded::class.java), - Hook { (it, state: UserProfileHeaderViewModel.ViewState.Loaded) -> - val id = state.user.id - if (userBadges.containsKey(id)) addUserBadges(id, it.thisObject) - else if (!fetchingBadges.getAndSet(true)) Utils.threadPool.execute { - try { - userBadges[id] = getUserBadges(Http.simpleJsonGet("$url/users/$id", UserBadges::class.java)) - addUserBadges(id, it.thisObject) - } catch (e: Throwable) { - if (e is Http.HttpException && e.statusCode == 404) - userBadges[id] = null - else - logger.error("Failed to get badges for user $id", e) - } finally { - fetchingBadges.set(false) - } - } - } - ) - - val badgeViewHolder = UserProfileHeaderView.BadgeViewHolder::class.java - val bindingField = badgeViewHolder.getDeclaredField("binding").apply { isAccessible = true } - Patcher.addPatch(badgeViewHolder.getDeclaredMethod("bind", Badge::class.java), Hook { (it, badge: Badge) -> - val url = badge.objectType - if (badge.icon == 0 && url != null) - (bindingField[it.thisObject] as UserProfileHeaderBadgeBinding).b.setImageUrl(url) - }) - - patcher.after("onViewBound", View::class.java) { - val binding = WidgetChannelsList.`access$getBinding$p`(this) - val toolbar = binding.g.parent as ViewGroup - if (toolbar.getChildAt(0).id != guildBadgeViewId) toolbar.addView(ImageView(toolbar.context).apply { - id = guildBadgeViewId - setPadding(0, 0, 4.dp, 0) - }, 0) - } - - patcher.after("configureHeaderIcons", Guild::class.java, Boolean::class.javaPrimitiveType!!) { (_, guild: Guild?) -> - val id = guild?.id ?: return@after - if (guildBadges.containsKey(id)) addGuildBadge(id, this) - else Utils.threadPool.execute { - try { - guildBadges[id] = Http.simpleJsonGet("$url/guilds/$id", CustomBadge::class.java) - } catch (e: Throwable) { - if (e is Http.HttpException && e.statusCode == 404) - guildBadges[id] = null - else - logger.error("Failed to get badges for guild $id", e) - } - Utils.mainThread.post { addGuildBadge(id, this) } - } - } - } - - private fun getUserBadges(badges: UserBadges): List { - val list = ArrayList(1) - badges.roles?.forEach { when(it) { - "dev" -> list.add(Badge(R.e.ic_staff_badge_blurple_24dp, null, "Aliucord Developer", false, null)) - "donor" -> list.add(Badge(0, null, "Aliucord Donor", false, "https://cdn.discordapp.com/emojis/859801776232202280.webp")) - "contributor" -> list.add(Badge(0, null, "Aliucord Contributor", false, "https://cdn.discordapp.com/emojis/886587553187246120.webp")) - } } - if (badges.custom?.isNotEmpty() == true) list.addAll(badges.custom.map { it.toDiscordBadge() }) - return list - } - - @Suppress("UNCHECKED_CAST") - private fun addUserBadges(id: Long, userProfileHeaderView: Any) { - val badges = userBadges[id] ?: return - val adapter = badgesAdapter[userProfileHeaderView] as SimpleRecyclerAdapter<*, *> - val data = dataField[adapter] as MutableList - data.addAll(badges) - } - - private var lastSetGuild: Long = 0 - private fun addGuildBadge(id: Long, widgetChannelsList: WidgetChannelsList) { - if (widgetChannelsList.view == null || lastSetGuild == id) return - lastSetGuild = id - val badge = guildBadges[id] - val binding = WidgetChannelsList.`access$getBinding$p`(widgetChannelsList) - val toolbar = binding.g.parent as ViewGroup - toolbar.findViewById(guildBadgeViewId)?.apply { - if (badge == null) visibility = View.GONE - else { - visibility = View.VISIBLE - with(badge.getDrawableId()) { if (this != 0) setImageResource(this) else setImageUrl(badge.url ?: "") } - setOnClickListener { Utils.showToast(badge.text) } - } - } - } - - private fun ImageView.setImageUrl(url: String) { - if (cache.containsKey(url)) setImageBitmap(cache[url]) - else Utils.threadPool.execute { - Http.Request(url).execute().stream().use { BitmapFactory.decodeStream(it) }.let { - cache[url] = it - Utils.mainThread.post { setImageBitmap(it) } - } - } - } - - override fun start(context: Context) {} - override fun stop(context: Context) {} -} diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/ButtonsAPI.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/ButtonsAPI.kt index 3490b3a84..bfae7ec72 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/ButtonsAPI.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/ButtonsAPI.kt @@ -8,9 +8,8 @@ package com.aliucord.coreplugins import android.content.Context import androidx.fragment.app.FragmentActivity -import com.aliucord.api.ButtonsAPI import com.aliucord.api.CommandsAPI -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.* import com.discord.models.message.Message @@ -19,7 +18,10 @@ import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemBotCompone import java.util.* import kotlin.text.startsWith -internal class ButtonsAPI : Plugin(Manifest("ButtonsAPI")) { +internal class ButtonsAPI : CorePlugin(Manifest("ButtonsAPI")) { + override val isHidden = true + override val isRequired = true + override fun load(context: Context) { patcher.before("onButtonComponentClick", Int::class.java, String::class.java) { (it, _: Any, customId: String) -> val acId = (-CommandsAPI.ALIUCORD_APP_ID).toString() diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/CommandHandler.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/CommandHandler.kt index c1a107ad7..8c909fb51 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/CommandHandler.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/CommandHandler.kt @@ -9,7 +9,7 @@ package com.aliucord.coreplugins import android.content.Context import android.widget.TextView import com.aliucord.api.CommandsAPI -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.* import com.discord.api.message.MessageTypes import com.discord.databinding.WidgetChatInputAutocompleteItemBinding @@ -28,7 +28,10 @@ import com.discord.widgets.chat.list.entries.MessageEntry import com.discord.widgets.chat.list.sheet.WidgetApplicationCommandBottomSheetViewModel @Suppress("UNCHECKED_CAST") -internal class CommandHandler : Plugin(Manifest("CommandHandler")) { +internal class CommandHandler : CorePlugin(Manifest("CommandHandler")) { + override val isHidden = true + override val isRequired = true + override fun load(context: Context) { Patcher.addPatch(BuiltInCommands::class.java, "getBuiltInCommands", emptyArray(), Hook { val list = it.result.run { if (this == null) return@Hook else this as MutableList } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/CoreCommands.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/CoreCommands.kt index ac64f7835..32e4afc5b 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/CoreCommands.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/CoreCommands.kt @@ -9,11 +9,22 @@ import android.os.Build import com.aliucord.* import com.aliucord.api.CommandsAPI import com.aliucord.api.CommandsAPI.CommandResult +import com.aliucord.entities.CorePlugin import com.aliucord.entities.Plugin import com.discord.api.commands.ApplicationCommandType import java.io.File -internal class CoreCommands : Plugin(Manifest("CoreCommands")) { +internal class CoreCommands : CorePlugin(Manifest("CoreCommands")) { + init { + manifest.description = "Adds basic slash commands to Aliucord for debugging purposes" + } + + private fun visiblePlugins(): Sequence { + return PluginManager.plugins + .values.asSequence() + .filter { it !is CorePlugin || !it.isHidden } + } + override fun start(context: Context) { commands.registerCommand( "echo", @@ -23,17 +34,6 @@ internal class CoreCommands : Plugin(Manifest("CoreCommands")) { CommandResult(it.getRequiredString("message"), null, false) } - commands.registerCommand( - "say", - "Sends message", - CommandsAPI.requiredMessageOption - ) { - CommandResult(it.getRequiredString("message")) - } - - fun formatPlugins(plugins: List, showVersions: Boolean): String = - plugins.joinToString(transform = { p -> p.getName() + if (showVersions) " (${p.manifest.version})" else "" }) - commands.registerCommand( "plugins", "Lists installed plugins", @@ -51,21 +51,20 @@ internal class CoreCommands : Plugin(Manifest("CoreCommands")) { ) ) { val showVersions = it.getBoolOrDefault("versions", false) + val (enabled, disabled) = visiblePlugins().partition(PluginManager::isPluginEnabled) - val plugins = PluginManager.plugins - val (enabled, disabled) = plugins.values.partition(PluginManager::isPluginEnabled) - val enabledStr = formatPlugins(enabled, showVersions) - val disabledStr = formatPlugins(disabled, showVersions) + fun formatPlugins(plugins: List): String = + plugins.joinToString { p -> if (showVersions && p !is CorePlugin) "${p.name} (${p.manifest.version})" else p.name } - if (plugins.isEmpty()) + if (enabled.isEmpty() && disabled.isEmpty()) CommandResult("No plugins installed", null, false) else CommandResult( """ **Enabled Plugins (${enabled.size}):** -${if (enabled.isEmpty()) "None" else "> $enabledStr"} +${if (enabled.isEmpty()) "None" else "> ${formatPlugins(enabled)}"} **Disabled Plugins (${disabled.size}):** -${if (disabled.isEmpty()) "None" else "> $disabledStr"} +${if (disabled.isEmpty()) "None" else "> ${formatPlugins(disabled)}"} """, null, it.getBoolOrDefault("send", false) @@ -73,11 +72,15 @@ ${if (disabled.isEmpty()) "None" else "> $disabledStr"} } commands.registerCommand("debug", "Posts debug info") { + val customPluginCount = PluginManager.plugins.values.count { it !is CorePlugin } + val enabledPluginCount = visiblePlugins().count(PluginManager::isPluginEnabled) + // .trimIndent() is broken sadly due to collision with Discord's Kotlin val str = """ **Debug Info:** > Discord: ${Constants.DISCORD_VERSION} -> Aliucord: ${BuildConfig.GIT_REVISION} (${PluginManager.plugins.size} plugins) +> Aliucord: ${BuildConfig.VERSION} ${if (BuildConfig.RELEASE) "" else "(Custom)"} +> Plugins: $customPluginCount installed, $enabledPluginCount total enabled > System: Android ${Build.VERSION.RELEASE} (SDK v${Build.VERSION.SDK_INT}) - ${getArchitecture()} > Rooted: ${getIsRooted() ?: "Unknown"} """ @@ -100,9 +103,12 @@ ${if (disabled.isEmpty()) "None" else "> $disabledStr"} "x86" -> return "i686" } } - return System.getProperty("os.arch") ?: System.getProperty("ro.product.cpu.abi") - ?: "Unknown Architecture" + return System.getProperty("os.arch") + ?: System.getProperty("ro.product.cpu.abi") + ?: "Unknown Architecture" } - override fun stop(context: Context) {} + override fun stop(context: Context) { + commands.unregisterAll() + } } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/CorePlugins.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/CorePlugins.kt deleted file mode 100644 index db385f82d..000000000 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/CorePlugins.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.aliucord.coreplugins - -import android.content.Context -import com.aliucord.coreplugins.plugindownloader.PluginDownloader -import com.aliucord.coreplugins.rn.RNAPI -import com.aliucord.coreplugins.slashcommandsfix.SlashCommandsFix -import com.aliucord.PluginManager - -/** CorePlugins Manager */ -object CorePlugins { - private var loaded = false - private var started = false - private val corePlugins = arrayOf( - RNAPI(), - Badges(), - CommandHandler(), - CoreCommands(), - NoTrack(), - PluginDownloader(), - SupportWarn(), - TokenLogin(), - ButtonsAPI(), - UploadSize(), - DefaultStickers(), - PrivateThreads(), - PrivateChannelsListScroll(), - MembersListFix(), - Pronouns(), - GifPreviewFix(), - SlashCommandsFix() - ) - - /** Loads all core plugins */ - @JvmStatic - fun loadAll(context: Context) { - check(!loaded) { "CorePlugins already loaded" } - loaded = true - for (p in corePlugins) { - PluginManager.logger.info("Loading core plugin: ${p.name}") - try { - p.load(context) - } catch (e: Throwable) { - PluginManager.logger.errorToast("Failed to load core plugin " + p.name, e) - } - } - } - - /** Starts all core plugins */ - @JvmStatic - fun startAll(context: Context) { - check(!started) { "CorePlugins already started" } - started = true - for (p in corePlugins) { - PluginManager.logger.info("Starting core plugin: ${p.name}") - try { - p.start(context) - } catch (e: Throwable) { - PluginManager.logger.errorToast("Failed to start core plugin " + p.name, e) - } - } - } -} diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/DefaultStickers.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/DefaultStickers.kt index 898fb06be..407cef6c5 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/DefaultStickers.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/DefaultStickers.kt @@ -8,7 +8,7 @@ package com.aliucord.coreplugins import android.content.Context import com.aliucord.Http -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.* import com.aliucord.utils.GsonUtils import com.discord.api.channel.Channel @@ -27,9 +27,12 @@ import de.robv.android.xposed.XC_MethodHook import rx.subjects.BehaviorSubject import java.util.Locale -internal class DefaultStickers : Plugin(Manifest("DefaultStickers")) { +internal class DefaultStickers : CorePlugin(Manifest("DefaultStickers")) { + override val isHidden = true + override val isRequired = true + @Suppress("UNCHECKED_CAST") - override fun load(context: Context) { + override fun start(context: Context) { val stickerPickerViewModel = StickerPickerViewModel::class.java val localeField = stickerPickerViewModel.getDeclaredField("locale").apply { isAccessible = true } Patcher.addPatch(stickerPickerViewModel.getDeclaredMethod("createCategoryItems", StickerPickerViewModel.StoreState.Loaded::class.java, List::class.java, List::class.java), object : XC_MethodHook() { @@ -78,6 +81,5 @@ internal class DefaultStickers : Plugin(Manifest("DefaultStickers")) { } } - override fun start(context: Context) {} override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/EnablePrivateThreads.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/EnablePrivateThreads.kt index 7b3f7cc3e..845a00522 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/EnablePrivateThreads.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/EnablePrivateThreads.kt @@ -10,15 +10,18 @@ import android.content.Context import android.view.View import android.widget.TextView import com.aliucord.Utils -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.after import com.aliucord.patcher.instead import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemThreadDraftForm import com.discord.widgets.chat.list.entries.ChatListEntry import com.discord.widgets.chat.list.entries.ThreadDraftFormEntry -internal class PrivateThreads : Plugin(Manifest("PrivateThreads")) { - override fun load(context: Context) { +internal class PrivateThreads : CorePlugin(Manifest("PrivateThreads")) { + override val isHidden = true + override val isRequired = true + + override fun start(context: Context) { patcher.instead("getCanCreatePrivateThread") { true } patcher.after("onConfigure", Int::class.javaPrimitiveType!!, ChatListEntry::class.java) { @@ -26,7 +29,5 @@ internal class PrivateThreads : Plugin(Manifest("PrivateThreads")) { } } - override fun start(context: Context?) {} - - override fun stop(context: Context?) {} + override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/ExperimentDefaults.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/ExperimentDefaults.kt new file mode 100644 index 000000000..4a1d66f68 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/ExperimentDefaults.kt @@ -0,0 +1,69 @@ +package com.aliucord.coreplugins + +import android.content.Context +import com.aliucord.entities.CorePlugin +import com.aliucord.patcher.* +import com.discord.stores.StoreExperiments +import com.discord.stores.StoreStream +import com.discord.utilities.features.GrowthTeamFeatures + +/** + * Discord wiped experiment defaults for Discord v126.21 so we have to set our own defaults now. + */ +internal class ExperimentDefaults : CorePlugin(Manifest("ExperimentDefaults")) { + override val isHidden = true + override val isRequired = true + + override fun start(context: Context) { + // Thanks Dolfies for helping with this + // Full list of v126.21 experiments: https://discord.com/channels/811255666990907402/811262084968742932/1276337938661118062 + val newOverrides = arrayOf( + "2020-09_threads" to 1, + "2020-12_guild_delete_feedback" to 0, // Analytics are not sent anyway, this is useless + "2021-02_view_threads" to 1, + "2021-03_android_extend_invite_expiration" to 1, // This matches with desktop + "2021-03_android_media_sink_wants" to 1, + "2021-03_stop_offscreen_video_streams" to 1, + "2021-04_contact_sync_android_main" to 1, // This system is deprecated I think but still functional? + "2021-06_desktop_school_hubs" to 1, + "2021-06_hub_email_connection" to 1, + "2021-06_reg_bailout_to_email_android" to 1, // Doesn't really matter + "2021-08_hub_multi_domain_mobile" to 1, + "2021-08_threads_permissions" to 1, + "2021-09_android_app_commands_frecency" to 1, + "2021-09_android_sms_autofill" to 1, + "2021-10_android_attachment_bottom_sheet" to 1, + "2021-10_study_group" to 1, + "2021-11_guild_communication_disabled_guilds" to 1, + "2021-11_guild_communication_disabled_users" to 1, + "2021-11_hub_events" to 1, + "2021-12_connected_accounts_playstation" to 2, // We don't want upsells + "2022-01_email_change_confirmation" to 1, + "2022-03_android_forum_channel_redesign" to 1, + "2022-03_highlights_settings" to 1, + "2022-03_text_in_voice" to 1, + + // 2021-10_premium_guild_member_profiles // We handle this manually + ) + + val experiments = StoreStream.getExperiments() + val overrides = StoreExperiments.`access$getExperimentOverrides$p`(experiments) + for ((key, value) in newOverrides) { + if (!overrides.containsKey(key)) + experiments.setOverride(key, value) + } + + // Silence the useless "Experiment Triggered: ..." message from logs + patcher.instead( + "isBucketEnabled", + Integer::class.java, + String::class.java, + Int::class.javaPrimitiveType!!, + Boolean::class.javaPrimitiveType!!, + ) { (_, assignedBucket: Int?, _: String, bucket: Int) -> + return@instead assignedBucket == bucket + } + } + + override fun stop(context: Context) {} +} diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/GifPreviewFix.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/GifPreviewFix.kt index 550222241..7e6e4454d 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/GifPreviewFix.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/GifPreviewFix.kt @@ -8,17 +8,21 @@ package com.aliucord.coreplugins import android.content.Context import android.net.Uri -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.after import com.discord.utilities.embed.EmbedResourceUtils -internal class GifPreviewFix : Plugin(Manifest("GifPreviewFix")) { +internal class GifPreviewFix : CorePlugin(Manifest("GifPreviewFix")) { + override val isHidden = true + override val isRequired = true + override fun load(context: Context) { patcher.after("getPreviewUrls", String::class.java, Int::class.java, Int::class.java, Boolean::class.java) { // it.args[3] is a boolean that indicates // if the gif should be animated (for example no autoplay setting) if (!(it.args[3] as Boolean)) return@after - var result = (it.result as List).toMutableList() + @Suppress("UNCHECKED_CAST") + val result = (it.result as List).toMutableList() val uri = Uri.parse(result[0]) if (uri.path?.endsWith(".gif") == true) { @@ -30,7 +34,6 @@ internal class GifPreviewFix : Plugin(Manifest("GifPreviewFix")) { } } - override fun start(context: Context?) {} - - override fun stop(context: Context?) {} + override fun start(context: Context) {} + override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/MembersListFix.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/MembersListFix.kt index 8f561f00b..7a7fede9a 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/MembersListFix.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/MembersListFix.kt @@ -7,24 +7,33 @@ package com.aliucord.coreplugins import android.content.Context -import com.aliucord.entities.Plugin -import com.aliucord.patcher.Hook -import com.aliucord.patcher.Patcher +import com.aliucord.entities.CorePlugin +import com.aliucord.patcher.after +import com.aliucord.utils.lazyField import com.discord.utilities.lazy.memberlist.ChannelMemberList import com.discord.utilities.lazy.memberlist.MemberListRow +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.forEach + +@Suppress("PrivatePropertyName") +internal class MembersListFix : CorePlugin(Manifest("MembersListFix")) { + private val f_memberListGroups by lazyField("groups") + + override val isHidden = true + override val isRequired = true -internal class MembersListFix : Plugin(Manifest("MembersListFix")) { @Suppress("UNCHECKED_CAST") - override fun load(context: Context?) { - val groups = ChannelMemberList::class.java.getDeclaredField("groups").apply { isAccessible = true } - Patcher.addPatch(ChannelMemberList::class.java.getDeclaredMethod("setGroups", List::class.java, Function1::class.java), Hook { - val list = it.thisObject as ChannelMemberList - val rows = list.rows - val groupsMap = groups[list] as Map - list.groupIndices.forEach { (idx, id) -> rows[idx] = groupsMap[id] } - }) + override fun load(context: Context) { + patcher.after("setGroups", List::class.java, Function1::class.java) { + val rows = this.rows + val groupsMap = f_memberListGroups[this] as Map + this.groupIndices.forEach { (idx, id) -> rows[idx] = groupsMap[id] } + } } - override fun start(context: Context?) {} - override fun stop(context: Context?) {} + override fun start(context: Context) {} + override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/NoTrack.java b/Aliucord/src/main/java/com/aliucord/coreplugins/NoTrack.java index 2e5de2b1b..8bfe55a68 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/NoTrack.java +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/NoTrack.java @@ -10,7 +10,7 @@ import android.content.Context; -import com.aliucord.entities.Plugin; +import com.aliucord.entities.CorePlugin; import com.aliucord.patcher.InsteadHook; import com.aliucord.patcher.Patcher; import com.discord.utilities.surveys.SurveyUtils; @@ -20,9 +20,15 @@ import de.robv.android.xposed.XposedBridge; -final class NoTrack extends Plugin { - NoTrack() { +public final class NoTrack extends CorePlugin { + public NoTrack() { super(new Manifest("NoTrack")); + getManifest().description = "Disables certain various app analytics and tracking"; + } + + @Override + public boolean isRequired() { + return true; } @Override diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/PrivateChannelsListScroll.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/PrivateChannelsListScroll.kt index cd0ed2ea8..64e9e0e9b 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/PrivateChannelsListScroll.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/PrivateChannelsListScroll.kt @@ -8,30 +8,28 @@ package com.aliucord.coreplugins import android.content.Context import androidx.recyclerview.widget.LinearLayoutManager -import com.aliucord.entities.Plugin -import com.aliucord.patcher.Hook -import com.aliucord.patcher.Patcher +import com.aliucord.entities.CorePlugin +import com.aliucord.patcher.* import com.discord.widgets.channels.list.WidgetChannelListModel import com.discord.widgets.channels.list.WidgetChannelsList -import de.robv.android.xposed.XC_MethodHook -internal class PrivateChannelsListScroll : Plugin(Manifest("PrivateChannelsListScroll")) { - var unhook: XC_MethodHook.Unhook? = null +internal class PrivateChannelsListScroll : CorePlugin(Manifest("PrivateChannelsListScroll")) { + override val isHidden = true + override val isRequired = true - override fun load(context: Context?) { - unhook = Patcher.addPatch(WidgetChannelsList::class.java.getDeclaredMethod("configureUI", WidgetChannelListModel::class.java), Hook { - val model = it.args[0] as WidgetChannelListModel + override fun load(context: Context) { + patcher.after("configureUI", WidgetChannelListModel::class.java) + { (_, model: WidgetChannelListModel) -> if (!model.isGuildSelected && model.items.size > 1) { - val manager = WidgetChannelsList.`access$getBinding$p`(it.thisObject as WidgetChannelsList).c.layoutManager!! as LinearLayoutManager + val manager = WidgetChannelsList.`access$getBinding$p`(this).c.layoutManager!! as LinearLayoutManager if (manager.findFirstVisibleItemPosition() != 0) { manager.scrollToPosition(0) - unhook?.unhook() - unhook = null + patcher.unpatchAll() } } - }) + } } - override fun start(context: Context?) {} - override fun stop(context: Context?) {} + override fun start(context: Context) {} + override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/Pronouns.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/Pronouns.kt index 4e8f81a88..f01abf11e 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/Pronouns.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/Pronouns.kt @@ -9,20 +9,23 @@ import androidx.core.content.res.ResourcesCompat import com.aliucord.Constants import com.aliucord.Utils import com.aliucord.api.rn.user.RNUserProfile -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.after import com.discord.utilities.view.text.SimpleDraweeSpanTextView import com.discord.widgets.user.profile.UserProfileHeaderView import com.discord.widgets.user.profile.UserProfileHeaderViewModel import com.lytefast.flexinput.R -val sheetProfileHeaderViewId = Utils.getResId("user_sheet_profile_header_view", "id") -val userProfileHeaderSecondaryNameViewId = Utils.getResId("user_profile_header_secondary_name", "id") +internal class Pronouns : CorePlugin(Manifest("Pronouns")) { + private val sheetProfileHeaderViewId = Utils.getResId("user_sheet_profile_header_view", "id") + private val userProfileHeaderSecondaryNameViewId = Utils.getResId("user_profile_header_secondary_name", "id") + private val pronounsViewId = View.generateViewId() -val pronounsViewId = View.generateViewId() + init { + manifest.description = "Display the new pronouns feature on user profiles" + } -internal class Pronouns : Plugin(Manifest("Pronouns")) { - override fun load(context: Context) { + override fun start(context: Context) { patcher.after("configureSecondaryName", UserProfileHeaderViewModel.ViewState.Loaded::class.java) { if (id != sheetProfileHeaderViewId) return@after val state = it.args[0] as? UserProfileHeaderViewModel.ViewState.Loaded ?: return@after @@ -47,7 +50,5 @@ internal class Pronouns : Plugin(Manifest("Pronouns")) { } } - override fun start(context: Context?) {} - - override fun stop(context: Context?) {} + override fun stop(context: Context) = patcher.unpatchAll() } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/SupportWarn.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/SupportWarn.kt index 1719d6953..2c2f3cb09 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/SupportWarn.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/SupportWarn.kt @@ -8,7 +8,7 @@ import android.widget.* import com.aliucord.Constants.PLUGIN_REQUESTS_CHANNEL_ID import com.aliucord.Utils import com.aliucord.api.SettingsAPI -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.fragments.InputDialog import com.aliucord.patcher.* import com.aliucord.settings.delegate @@ -17,11 +17,18 @@ import com.discord.widgets.chat.input.WidgetChatInput import com.lytefast.flexinput.R @SuppressLint("SetTextI18n") -internal class SupportWarn : Plugin(Manifest("SupportWarn")) { +internal class SupportWarn : CorePlugin(Manifest("SupportWarn")) { private val SettingsAPI.acceptedPrdNotRequests: Boolean by settings.delegate(false) private val SettingsAPI.acceptedDevNotSupport: Boolean by settings.delegate(false) - override fun load(context: Context) { + init { + manifest.description = "Show a warning prior to interacting with the Aliucord server" + } + + // Allow this to be disabled once warning has been acknowledged + override val isRequired get() = !settings.acceptedDevNotSupport + + override fun start(context: Context) { if (settings.acceptedPrdNotRequests && settings.acceptedDevNotSupport) return val channelList = listOf( @@ -39,18 +46,19 @@ internal class SupportWarn : Plugin(Manifest("SupportWarn")) { val gateButtonArrowId = Utils.getResId("chat_input_member_verification_guard_action", "id") val gateButtonLayoutId = Utils.getResId("guard_member_verification", "id") - Patcher.addPatch(WidgetChatInput::class.java.getDeclaredMethod("configureChatGuard", ChatInputViewModel.ViewState.Loaded::class.java), Hook { (it, loaded: ChatInputViewModel.ViewState.Loaded) -> - if (loaded.channelId !in channelList || loaded.shouldShowVerificationGate) return@Hook + patcher.after("configureChatGuard", ChatInputViewModel.ViewState.Loaded::class.java) + { (_, loaded: ChatInputViewModel.ViewState.Loaded) -> + if (loaded.channelId !in channelList || loaded.shouldShowVerificationGate) return@after val (text, desc, key) = if (loaded.channelId == PLUGIN_REQUESTS_CHANNEL_ID) { - if (settings.acceptedPrdNotRequests) return@Hook + if (settings.acceptedPrdNotRequests) return@after Triple( "PLEASE READ: This is not a request channel, do not request plugins!", "This is NOT A REQUESTING CHANNEL. For information on how to request a plugin, check the pins in this channel. If you have read this, type \"I understand\" into the box.", "acceptedPrdNotRequests" ) } else { - if (settings.acceptedDevNotSupport) return@Hook + if (settings.acceptedDevNotSupport) return@after Triple( "PLEASE READ: This is not a support channel, do not ask for help!", "This is NOT A SUPPORT CHANNEL. Do NOT ask for help about using or installing a plugin or theme here or you will be muted. If you have read this, type \"I understand\" into the box.", @@ -58,8 +66,7 @@ internal class SupportWarn : Plugin(Manifest("SupportWarn")) { ) } - val thisObject = it.thisObject as WidgetChatInput - val root = WidgetChatInput.`access$getBinding$p`(thisObject).root + val root = WidgetChatInput.`access$getBinding$p`(this).root val gateButtonLayout = root.findViewById(gateButtonLayoutId) val chatWrap = root.findViewById(chatWrapId) @@ -83,11 +90,10 @@ internal class SupportWarn : Plugin(Manifest("SupportWarn")) { dialog.dismiss() } - dialog.show(thisObject.parentFragmentManager, "Warning") + dialog.show(this.parentFragmentManager, "Warning") } - }) + } } - override fun start(context: Context) {} - override fun stop(context: Context) {} + override fun stop(context: Context) = patcher.unpatchAll() } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/TokenLogin.java b/Aliucord/src/main/java/com/aliucord/coreplugins/TokenLogin.java index 5b9b429bb..a1f9b3892 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/TokenLogin.java +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/TokenLogin.java @@ -13,7 +13,7 @@ import android.widget.RelativeLayout; import com.aliucord.Utils; -import com.aliucord.entities.Plugin; +import com.aliucord.entities.CorePlugin; import com.aliucord.patcher.Patcher; import com.aliucord.patcher.Hook; import com.aliucord.utils.DimenUtils; @@ -33,9 +33,10 @@ import kotlin.Unit; -final class TokenLogin extends Plugin { - TokenLogin() { +public final class TokenLogin extends CorePlugin { + public TokenLogin() { super(new Manifest("TokenLogin")); + getManifest().description = "Provide functionality to log in with a token directly from the login screen"; } public static class Page extends AppFragment { diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/UploadSize.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/UploadSize.kt index e72934347..303c34646 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/UploadSize.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/UploadSize.kt @@ -10,7 +10,7 @@ import android.content.ContentResolver import android.content.Context import android.view.View import com.aliucord.Http -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.* import com.aliucord.utils.GsonUtils import com.aliucord.utils.RNSuperProperties @@ -29,7 +29,10 @@ import de.robv.android.xposed.XposedBridge import rx.subjects.BehaviorSubject import b.a.a.c as ImageUploadFailedDialog -internal class UploadSize : Plugin(Manifest("UploadSize")) { +internal class UploadSize : CorePlugin(Manifest("UploadSize")) { + override val isHidden = true + override val isRequired = true + @Suppress("PropertyName", "unused") private companion object { const val DEFAULT_MAX_FILE_SIZE = 25 @@ -57,7 +60,7 @@ internal class UploadSize : Plugin(Manifest("UploadSize")) { } @Suppress("UNCHECKED_CAST") - override fun load(context: Context?) { + override fun start(context: Context) { patcher.instead("getGuildMaxFileSizeMB", Int::class.java) { (_, tier: Int) -> when (tier) { 2 -> 50 @@ -167,6 +170,5 @@ internal class UploadSize : Plugin(Manifest("UploadSize")) { } } - override fun start(context: Context?) {} - override fun stop(context: Context?) {} + override fun stop(context: Context) {} } diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/badges/BadgesAPI.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/BadgesAPI.kt new file mode 100644 index 000000000..c19bebd78 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/BadgesAPI.kt @@ -0,0 +1,68 @@ +package com.aliucord.coreplugins.badges + +import com.aliucord.* +import com.aliucord.api.SettingsAPI +import com.aliucord.settings.delegate +import kotlin.time.* + +internal class BadgesAPI(private val settings: SettingsAPI) { + private var SettingsAPI.cacheExpiration by settings.delegate(0L) + private var SettingsAPI.cachedBadges by settings.delegate(BadgesInfo(emptyMap(), emptyMap())) + + /** + * Get cached badge data or re-fetch from the API if expired. + */ + @Suppress("DEPRECATION") + @OptIn(ExperimentalTime::class) + fun getBadges(): BadgesInfo { + if (!isCacheExpired()) return settings.cachedBadges + + val data = fetchBadges() + + return if (data != null) { + settings.cacheExpiration = System.currentTimeMillis() + 1.days.inWholeMilliseconds + settings.cachedBadges = data + data + } else { + // Failed to fetch; keep cache and try later + settings.cacheExpiration = System.currentTimeMillis() + 6.hours.inWholeMilliseconds + settings.cachedBadges + } + } + + /** + * Fetch badge data from the API directly. + */ + private fun fetchBadges(): BadgesInfo? { + return try { + Http.Request("https://aliucord.com/files/badges/data.json") + .setHeader("User-Agent", "Aliucord/${BuildConfig.VERSION}") + .execute() + .json(BadgesInfo::class.java) + } catch (e: Exception) { + Logger("BadgesAPI").error("Failed to fetch supporter badges!", e) + null + } + } + + private fun isCacheExpired(): Boolean = + settings.cacheExpiration <= System.currentTimeMillis() +} + +private typealias Snowflake = Long +private typealias BadgeName = String + +internal data class BadgesInfo( + val guilds: Map, + val users: Map, +) + +internal data class BadgesUserData( + val roles: List?, + val custom: List?, +) + +internal data class BadgeData( + val url: String, + val text: String, +) diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/badges/SupporterBadges.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/SupporterBadges.kt new file mode 100644 index 000000000..4d2961a15 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/SupporterBadges.kt @@ -0,0 +1,128 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.badges + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.aliucord.* +import com.aliucord.entities.CorePlugin +import com.aliucord.patcher.* +import com.aliucord.utils.DimenUtils.dp +import com.aliucord.utils.lazyField +import com.discord.databinding.UserProfileHeaderBadgeBinding +import com.discord.models.guild.Guild +import com.discord.utilities.views.SimpleRecyclerAdapter +import com.discord.widgets.channels.list.WidgetChannelsList +import com.discord.widgets.user.Badge +import com.discord.widgets.user.profile.UserProfileHeaderView +import com.discord.widgets.user.profile.UserProfileHeaderViewModel +import com.lytefast.flexinput.R + +@Suppress("PrivatePropertyName") +internal class SupporterBadges : CorePlugin(MANIFEST) { + /** Used for the badge in the guild channel list header */ + private val guildBadgeViewId = View.generateViewId() + + /** Badges info that is populated upon plugin start */ + private var badges: BadgesInfo? = null + + // Cached fields + private val f_badgesAdapter by lazyField("badgesAdapter") + private val f_recyclerAdapterData by lazyField>("data") + private val f_badgeViewHolderBinding by lazyField("binding") + + @Suppress("UNCHECKED_CAST") + override fun start(context: Context) { + Utils.threadPool.execute { + badges = BadgesAPI(settings).getBadges() + } + + // Add badges to the RecyclerView data for badges in the user profile header + patcher.after("updateViewState", UserProfileHeaderViewModel.ViewState.Loaded::class.java) + { (_, state: UserProfileHeaderViewModel.ViewState.Loaded) -> + val userBadgesData = badges?.users?.get(state.user.id) ?: return@after + val roleBadges = userBadgesData.roles?.mapNotNull(::getBadgeForRole) ?: emptyList() + val customBadges = userBadgesData.custom?.map(::getBadgeForCustom) ?: emptyList() + + val adapter = f_badgesAdapter[this] as SimpleRecyclerAdapter + val data = f_recyclerAdapterData[adapter] as MutableList + data.addAll(roleBadges) + data.addAll(customBadges) + } + + // Set image url for badge ImageViews + patcher.after("bind", Badge::class.java) + { (_, badge: Badge) -> + // Image URL is smuggled through the objectType property + val url = badge.objectType + + // Check that badge is ours + if (badge.icon != 0 || url == null) return@after + + val binding = f_badgeViewHolderBinding[this] as UserProfileHeaderBadgeBinding + val imageView = binding.b + imageView.setCacheableImage(url) + } + + // Add blank ImageView to the channels list + patcher.after("onViewBound", View::class.java) { + val binding = WidgetChannelsList.`access$getBinding$p`(this) + val toolbar = binding.g.parent as ViewGroup + val imageView = ImageView(toolbar.context).apply { + id = guildBadgeViewId + setPadding(0, 0, 4.dp, 0) + } + + if (toolbar.getChildAt(0).id != guildBadgeViewId) + toolbar.addView(imageView, 0) + } + + // Configure the channels list's newly added ImageView to show target guild badge + patcher.after("configureHeaderIcons", Guild::class.java, Boolean::class.javaPrimitiveType!!) + { (_, guild: Guild?) -> + val badgeData = guild?.id?.let { id -> badges?.guilds?.get(id) } + + if (this.view == null) return@after + val binding = WidgetChannelsList.`access$getBinding$p`(this) + val toolbar = binding.g.parent as ViewGroup + + toolbar.findViewById(guildBadgeViewId)?.apply { + if (badgeData == null) visibility = View.GONE + else { + visibility = View.VISIBLE + setCacheableImage(badgeData.url) + setOnClickListener { Utils.showToast(badgeData.text) } + } + } + } + } + + override fun stop(context: Context) {} + + private companion object { + val MANIFEST = Manifest().apply { + name = "SupporterBadges" + description = "Show badges in the profiles of contributors and donors ♡" + } + + val DEV_BADGE = Badge(R.e.ic_staff_badge_blurple_24dp, null, "Aliucord Developer", false, null) + val DONOR_BADGE = Badge(0, null, "Aliucord Donor", false, "https://cdn.discordapp.com/emojis/859801776232202280.webp") + val CONTRIB_BADGE = Badge(0, null, "Aliucord Contributor", false, "https://cdn.discordapp.com/emojis/886587553187246120.webp") + + fun getBadgeForRole(role: String): Badge? = when (role) { + "dev" -> DEV_BADGE + "donor" -> DONOR_BADGE + "contributor" -> CONTRIB_BADGE + else -> null + } + + fun getBadgeForCustom(data: BadgeData): Badge = + Badge(0, null, data.text, false, data.url) + } +} diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/badges/Utils.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/Utils.kt new file mode 100644 index 000000000..5a010e6c1 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/badges/Utils.kt @@ -0,0 +1,38 @@ +package com.aliucord.coreplugins.badges + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.widget.ImageView +import com.aliucord.* + +private val imageCache = HashMap() + +/** + * Set a remote image to be displayed in this view. + * This will be cached in memory for the lifecycle of the application. + * As such, this should only be used for long-living and often used images. + */ +internal fun ImageView.setCacheableImage(url: String) { + val cachedImage = imageCache[url] + + if (cachedImage != null) { + setImageBitmap(cachedImage) + } else { + Utils.threadPool.execute { + try { + val image = Http.Request(url) + .setHeader("User-Agent", "Aliucord/${BuildConfig.VERSION}") + .execute() + .stream() + .let(BitmapFactory::decodeStream) + + Utils.mainThread.post { + imageCache[url] = image + setImageBitmap(image) + } + } catch (e: Exception) { + Logger("ImageCache").warn("Failed to retrieve image $url", e) + } + } + } +} diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/plugindownloader/PluginDownloader.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/plugindownloader/PluginDownloader.kt index 1f1119a9b..19166baca 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/plugindownloader/PluginDownloader.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/plugindownloader/PluginDownloader.kt @@ -15,7 +15,7 @@ import androidx.core.content.ContextCompat import com.aliucord.Constants.* import com.aliucord.Logger import com.aliucord.Utils -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.aliucord.patcher.Hook import com.aliucord.patcher.component1 import com.aliucord.patcher.component2 @@ -33,8 +33,12 @@ private val repoPattern = Pattern.compile("https?://github\\.com/([A-Za-z0-9\\-_ private val zipPattern = Pattern.compile("https?://(?:github|raw\\.githubusercontent)\\.com/([A-Za-z0-9\\-_.]+)/([A-Za-z0-9\\-_.]+)/(?:raw|blob)?/?\\w+/(\\w+).zip") -internal class PluginDownloader : Plugin(Manifest("PluginDownloader")) { +internal class PluginDownloader : CorePlugin(Manifest("PluginDownloader")) { + override val isRequired = true + init { + manifest.description = "Utility for installing plugins directly from the Aliucord server's plugins channels" + PluginFile("PluginDownloader").takeIf { it.exists() }?.let { if (it.delete()) Utils.showToast("PluginDownloader has been merged into Aliucord, so I deleted the plugin for you.", true) diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/rn/RNAPI.kt b/Aliucord/src/main/java/com/aliucord/coreplugins/rn/RNAPI.kt index 73a420d08..2031cead3 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/rn/RNAPI.kt +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/rn/RNAPI.kt @@ -7,14 +7,17 @@ package com.aliucord.coreplugins.rn import android.content.Context -import com.aliucord.entities.Plugin +import com.aliucord.entities.CorePlugin import com.discord.api.channel.Channel import com.discord.api.user.User import com.discord.api.user.UserProfile import com.discord.models.message.Message import de.robv.android.xposed.XposedBridge -class RNAPI : Plugin(Manifest("RNAPI")) { +internal class RNAPI : CorePlugin(Manifest("RNAPI")) { + override val isHidden = true + override val isRequired = true + override fun load(context: Context?) { XposedBridge.makeClassInheritable(Channel::class.java) XposedBridge.makeClassInheritable(Message::class.java) diff --git a/Aliucord/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java b/Aliucord/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java index e75564bfa..c8580c582 100644 --- a/Aliucord/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java +++ b/Aliucord/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java @@ -7,12 +7,13 @@ package com.aliucord.coreplugins.slashcommandsfix; import android.content.Context; -import com.aliucord.entities.Plugin; +import com.aliucord.entities.CorePlugin; import de.robv.android.xposed.XposedBridge; -final public class SlashCommandsFix extends Plugin { +final public class SlashCommandsFix extends CorePlugin { public SlashCommandsFix() { super(new Manifest("SlashCommandsFix")); + this.getManifest().description = "Fixes slash commands by switching to the new API"; } @Override diff --git a/Aliucord/src/main/java/com/aliucord/entities/CorePlugin.kt b/Aliucord/src/main/java/com/aliucord/entities/CorePlugin.kt new file mode 100644 index 000000000..cdbabc898 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/entities/CorePlugin.kt @@ -0,0 +1,14 @@ +package com.aliucord.entities + +internal abstract class CorePlugin(manifest: Manifest) : Plugin(manifest) { + /** + * Hides this core plugin from the plugins page. + * This should generally be used for coreplugins that are fixing existing functionality. + */ + open val isHidden: Boolean = false + + /** + * Whether this core plugin cannot be disabled at all. + */ + open val isRequired: Boolean = false +} diff --git a/Aliucord/src/main/java/com/aliucord/entities/Plugin.java b/Aliucord/src/main/java/com/aliucord/entities/Plugin.java index 79d7177cb..278682ff2 100644 --- a/Aliucord/src/main/java/com/aliucord/entities/Plugin.java +++ b/Aliucord/src/main/java/com/aliucord/entities/Plugin.java @@ -32,6 +32,8 @@ public static class Author { public String name; /** The id of the plugin author */ public long id; + /** Whether to hyperlink the profile specified by {@link id} */ + public boolean hyperlink; /** * Constructs an Author with the specified name and an ID of 0 @@ -46,8 +48,16 @@ public Author(String name) { * @param id The id of the author */ public Author(String name, long id) { + this(name, id, true); + } + + /** + * Constructs an Author with the specified name, ID, and whether to hyperlink the profile. + */ + public Author(String name, long id, boolean hyperlink) { this.name = name; this.id = id; + this.hyperlink = hyperlink; } @NonNull diff --git a/Aliucord/src/main/java/com/aliucord/settings/Plugins.java b/Aliucord/src/main/java/com/aliucord/settings/Plugins.java index dfacf027e..eb7848ebf 100644 --- a/Aliucord/src/main/java/com/aliucord/settings/Plugins.java +++ b/Aliucord/src/main/java/com/aliucord/settings/Plugins.java @@ -13,8 +13,7 @@ import android.graphics.drawable.shapes.RectShape; import android.text.*; import android.text.style.ClickableSpan; -import android.view.View; -import android.view.ViewGroup; +import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -22,6 +21,7 @@ import androidx.recyclerview.widget.*; import com.aliucord.*; +import com.aliucord.entities.CorePlugin; import com.aliucord.entities.Plugin; import com.aliucord.fragments.ConfirmDialog; import com.aliucord.fragments.SettingsPage; @@ -37,6 +37,8 @@ import java.io.File; import java.util.*; +import kotlin.comparisons.ComparisonsKt; + public class Plugins extends SettingsPage { public static class Adapter extends RecyclerView.Adapter implements Filterable { public static final class ViewHolder extends RecyclerView.ViewHolder { @@ -85,6 +87,7 @@ public void onUninstallClick(View view) { private final Context ctx; private final List originalData; private List data; + public boolean showBuiltIn = false; public Adapter(AppFragment fragment, Collection plugins) { super(); @@ -93,9 +96,13 @@ public Adapter(AppFragment fragment, Collection plugins) { ctx = fragment.requireContext(); this.originalData = new ArrayList<>(plugins); - originalData.sort(Comparator.comparing(Plugin::getName)); + originalData.removeIf(p -> p instanceof CorePlugin && ((CorePlugin)p).isHidden()); + originalData.sort(ComparisonsKt.compareBy( + p -> p instanceof CorePlugin, // coreplugins last + Plugin::getName // Natural order by title + )); - data = originalData; + data = CollectionUtils.filter(originalData, Adapter::filterCorePlugins); } @Override @@ -116,15 +123,20 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { boolean enabled = PluginManager.isPluginEnabled(p.getName()); holder.card.switchHeader.setChecked(enabled); - holder.card.descriptionView.setText(p.getManifest().description); + holder.card.switchHeader.setButtonVisibility(!(p instanceof CorePlugin) || !((CorePlugin) p).isRequired()); + holder.card.descriptionView.setText(MDUtils.render(p.getManifest().description)); holder.card.settingsButton.setVisibility(p.settingsTab != null ? View.VISIBLE : View.GONE); holder.card.settingsButton.setEnabled(enabled); + holder.card.uninstallButton.setVisibility(p.__filename != null ? View.VISIBLE : View.GONE); + holder.card.repoButton.setVisibility(p.getManifest().updateUrl != null ? View.VISIBLE : View.GONE); holder.card.changeLogButton.setVisibility(p.getManifest().changelog != null ? View.VISIBLE : View.GONE); - String title = String.format("%s v%s by %s", p.getName(), manifest.version, TextUtils.join(", ", manifest.authors)); + String title = p instanceof CorePlugin + ? String.format("%s [BUILT-IN]", p.getName()) + : String.format("%s v%s by %s", p.getName(), manifest.version, TextUtils.join(", ", manifest.authors)); SpannableString spannableTitle = new SpannableString(title); for (Plugin.Manifest.Author author : manifest.authors) { - if (author.id < 1) continue; + if (author.id < 1 || !author.hyperlink) continue; int i = title.indexOf(author.name, p.getName().length() + 2 + manifest.version.length() + 3); spannableTitle.setSpan(new ClickableSpan() { @Override @@ -141,19 +153,20 @@ public void onClick(@NonNull View widget) { @Override protected FilterResults performFiltering(CharSequence constraint) { List resultsList; - if (constraint == null || constraint.equals("")) - resultsList = originalData; - else { + if (constraint == null || constraint.equals("")) { + if (showBuiltIn) resultsList = originalData; + else resultsList = CollectionUtils.filter(originalData, Adapter::filterCorePlugins); + } else { String search = constraint.toString().toLowerCase().trim(); resultsList = CollectionUtils.filter(originalData, p -> { - if (p.getName().toLowerCase().contains(search)) return true; - Plugin.Manifest manifest = p.getManifest(); - if (manifest.description.toLowerCase().contains(search)) return true; - for (Plugin.Manifest.Author author : manifest.authors) - if (author.name.toLowerCase().contains(search)) return true; - return false; - } - ); + if (!showBuiltIn && p instanceof CorePlugin) return false; + if (p.getName().toLowerCase().contains(search)) return true; + Plugin.Manifest manifest = p.getManifest(); + if (manifest.description.toLowerCase().contains(search)) return true; + for (Plugin.Manifest.Author author : manifest.authors) + if (author.name.toLowerCase().contains(search)) return true; + return false; + }); } FilterResults results = new FilterResults(); results.values = resultsList; @@ -262,6 +275,10 @@ public void onUninstallClick(int position) { dialog.show(fragment.getParentFragmentManager(), "Confirm Plugin Uninstall"); } + + public static boolean filterCorePlugins(Plugin p) { + return !(p instanceof CorePlugin); + } } @Override @@ -320,5 +337,17 @@ public void afterTextChanged(Editable s) { public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { } }); + + getHeaderBar().getMenu() + .add("Show built-in") + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER) + .setCheckable(true) + .setOnMenuItemClickListener(item -> { + var show = !adapter.showBuiltIn; + adapter.showBuiltIn = show; + adapter.getFilter().filter(editText.getText()); + item.setChecked(show); + return true; + }); } } diff --git a/Aliucord/src/main/java/com/aliucord/updater/ManagerBuild.kt b/Aliucord/src/main/java/com/aliucord/updater/ManagerBuild.kt new file mode 100644 index 000000000..bea82d37a --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/updater/ManagerBuild.kt @@ -0,0 +1,66 @@ +package com.aliucord.updater + +import com.aliucord.Logger +import com.aliucord.utils.GsonUtils +import com.aliucord.utils.GsonUtils.fromJson +import com.aliucord.utils.SemVer +import java.lang.Exception + +/** + * Version checking for various install-time utilities that were used to install the app. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ManagerBuild { + var metadata: InstallMetadata? = null + private set + + /** + * Whether this installation has *at least* a specific version of smali patches applied to it. + */ + @JvmStatic + fun hasPatches(version: String): Boolean = + (metadata?.patchesVersion ?: SemVer.Zero) >= SemVer.parse(version) + + /** + * Whether this installation has *at least* a specific version of injector added to it. + */ + @JvmStatic + fun hasInjector(version: String): Boolean = + (metadata?.injectorVersion ?: SemVer.Zero) >= SemVer.parse(version) + + /** + * Whether this installation was patched with *at least* a specific version of manager + * @param version If null, then any version matches. + */ + @JvmStatic + fun installedWithManager(version: String?): Boolean { + return if (version == null) { + metadata != null + } else { + (metadata?.managerVersion ?: SemVer.Zero) >= SemVer.parse(version) + } + } + + init { + try { + // Manager adds this to the APK root + val stream = this.javaClass.classLoader?.getResourceAsStream("aliucord.json") + + if (stream != null) { + metadata = GsonUtils.gson.fromJson( + stream.bufferedReader(), + InstallMetadata::class.java + ) + } + } catch (e: Exception) { + Logger("ManagerBuild").warn("Failed to parse Manager install metadata", e) + } + } + + data class InstallMetadata( + val customManager: Boolean, + val managerVersion: SemVer, + val injectorVersion: SemVer, + val patchesVersion: SemVer, + ) +} diff --git a/Aliucord/src/main/java/com/aliucord/updater/Updater.java b/Aliucord/src/main/java/com/aliucord/updater/Updater.java index 7bd5bb7b0..bcb1b1bf1 100644 --- a/Aliucord/src/main/java/com/aliucord/updater/Updater.java +++ b/Aliucord/src/main/java/com/aliucord/updater/Updater.java @@ -16,14 +16,14 @@ public class Updater { /** - * Compares two versions of a plugin to determine whether it is outdated + * Compares two SemVer-style versions to determine whether a component is outdated * - * @param plugin The name of the plugin + * @param component The name of the plugin * @param version The local version of the plugin * @param newVersion The latest version of the plugin * @return Whether newVersion is newer than version */ - public static boolean isOutdated(String plugin, String version, String newVersion) { + public static boolean isOutdated(String component, String version, String newVersion) { try { String[] versions = version.split("\\."); String[] newVersions = newVersion.split("\\."); @@ -36,14 +36,14 @@ public static boolean isOutdated(String plugin, String version, String newVersio if (newInt < oldInt) return false; } } catch (NullPointerException | NumberFormatException th) { - PluginUpdater.logger.error(String.format("Failed to check updates for plugin %s due to an invalid updater/manifest version", plugin), th); + PluginUpdater.logger.error(String.format("Failed to check updates for %s due to an invalid updater/manifest version", component), th); } return false; } private static class AliucordData { - public String aliucordHash; + public String coreVersion; public int versionCode; } @@ -54,7 +54,7 @@ private static class AliucordData { private static boolean fetchAliucordData() { try (var req = new Http.Request("https://raw.githubusercontent.com/Aliucord/Aliucord/builds/data.json")) { var res = req.execute().json(AliucordData.class); - isAliucordOutdated = !BuildConfig.GIT_REVISION.equals(res.aliucordHash); + isAliucordOutdated = isOutdated("Aliucord", BuildConfig.VERSION, res.coreVersion); isDiscordOutdated = Constants.DISCORD_VERSION < res.versionCode; return true; } catch (IOException ex) { diff --git a/Aliucord/src/main/java/com/aliucord/utils/GsonUtils.kt b/Aliucord/src/main/java/com/aliucord/utils/GsonUtils.kt index 6715d8999..acc91e547 100644 --- a/Aliucord/src/main/java/com/aliucord/utils/GsonUtils.kt +++ b/Aliucord/src/main/java/com/aliucord/utils/GsonUtils.kt @@ -21,6 +21,7 @@ import b.i.d.e as GsonBuilder * [Original source](https://github.com/google/gson/blob/main/gson/src/main/java/com/google/gson/annotations/SerializedName.java) */ typealias SerializedName = b.i.d.p.b +typealias JsonAdapter = b.i.d.p.a object GsonUtils { diff --git a/Aliucord/src/main/java/com/aliucord/utils/SemVer.kt b/Aliucord/src/main/java/com/aliucord/utils/SemVer.kt new file mode 100644 index 000000000..a1aefb992 --- /dev/null +++ b/Aliucord/src/main/java/com/aliucord/utils/SemVer.kt @@ -0,0 +1,85 @@ +package com.aliucord.utils + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.lang.IllegalArgumentException + +/** + * Parses a Semantic version in the format of `v1.0.0` + */ +@JsonAdapter(SemVer.Adapter::class) +data class SemVer( + val major: Int, + val minor: Int, + val patch: Int, +) : Comparable { + override fun compareTo(other: SemVer): Int { + val pairs = arrayOf( + major to other.major, + minor to other.minor, + patch to other.patch, + ) + + return pairs + .map { (first, second) -> first.compareTo(second) } + .find { it != 0 } + ?: 0 + } + + override fun equals(other: Any?): Boolean { + val ver = other as? SemVer + ?: return false + + return ver.major == major && + ver.minor == minor && + ver.patch == patch + } + + override fun toString(): String { + return "$major.$minor.$patch" + } + + override fun hashCode(): Int { + var result = major + result = 31 * result + minor + result = 31 * result + patch + return result + } + + companion object { + @JvmField + val Zero = SemVer(0, 0, 0) + + @JvmStatic + fun parseOrNull(version: String?): SemVer? { + // Handle 'v' prefix + val versionString = version?.removePrefix("v") + ?: return null + + val parts = versionString + .split(".") + .mapNotNull { it.toIntOrNull() } + .takeIf { it.size == 3 } + ?: return null + + return SemVer(parts[0], parts[1], parts[2]) + } + + @JvmStatic + fun parse(version: String): SemVer = parseOrNull(version) + ?: throw IllegalArgumentException("Invalid semver string $version") + } + + class Adapter : TypeAdapter() { + override fun read(input: JsonReader): SemVer { + val string = input.J() // nextString() + return parse(string) + } + + override fun write(output: JsonWriter, value: SemVer) { + val string = value.toString() + output.H(string) // encode string + } + } +} diff --git a/Aliucord/src/main/java/com/aliucord/widgets/PluginCard.java b/Aliucord/src/main/java/com/aliucord/widgets/PluginCard.java index 5b32327e6..0a2f0592b 100644 --- a/Aliucord/src/main/java/com/aliucord/widgets/PluginCard.java +++ b/Aliucord/src/main/java/com/aliucord/widgets/PluginCard.java @@ -72,7 +72,7 @@ public PluginCard(Context ctx) { buttonLayout.setRowCount(1); buttonLayout.setColumnCount(5); buttonLayout.setUseDefaultMargins(true); - buttonLayout.setPadding(p2, 0, p2, 0); + buttonLayout.setPadding(p, p2, p, 0); settingsButton = new Button(ctx); settingsButton.setText("Settings"); diff --git a/Injector/build.gradle.kts b/Injector/build.gradle.kts index 34fcfb510..cf67e7fcb 100644 --- a/Injector/build.gradle.kts +++ b/Injector/build.gradle.kts @@ -1,5 +1,7 @@ @file:Suppress("UnstableApiUsage") +version = "2.0.0" + aliucord { projectType.set(com.aliucord.gradle.ProjectType.INJECTOR) } diff --git a/build.gradle.kts b/build.gradle.kts index 7ab4bee70..ad6844cb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,8 @@ plugins { } subprojects { + if (project.name !in arrayOf("Aliucord", "Injector")) return@subprojects + apply { plugin("com.android.library") plugin("kotlin-android") diff --git a/gradle.properties b/gradle.properties index 01b80d70c..c09e1e3b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,5 +15,3 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 386194d03..ea803cd68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ android-gradle = "7.0.4" appcompat = "1.4.1" dokka = "1.8.10" aliuhook = "fb7bf41" -aliucord-gradle = "bbcd8a8" +aliucord-gradle = "5abb762" discord = "126021" #noinspection GradleDependency causes errors material = "1.5.0" diff --git a/installer/android/app/build.gradle b/installer/android/app/build.gradle index 76df0b607..9d15d5770 100644 --- a/installer/android/app/build.gradle +++ b/installer/android/app/build.gradle @@ -1,29 +1,8 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' +plugins { + id "com.android.application" + id "dev.flutter.flutter-gradle-plugin" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def getGitHash = { -> def stdout = new ByteArrayOutputStream() exec { @@ -41,15 +20,16 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + namespace = "com.aliucord.installer" + compileSdk = 34 defaultConfig { applicationId "com.aliucord.installer" - minSdkVersion 21 + minSdk = 21 //noinspection OldTargetApi - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + targetSdk = 29 + versionCode = flutter.versionCode + versionName = flutter.versionName buildConfigField "String", "GIT_REVISION", "\"${getGitHash()}\"" } @@ -65,11 +45,14 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' shrinkResources false signingConfig signingConfigs.release } } + + buildFeatures.buildConfig = true } flutter { diff --git a/installer/android/app/proguard-rules.pro b/installer/android/app/proguard-rules.pro new file mode 100644 index 000000000..06e1d838d --- /dev/null +++ b/installer/android/app/proguard-rules.pro @@ -0,0 +1,19 @@ +-keep class com.aliucord.** { *; } + +-keepclasseswithmembernames class * { + native ; +} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# Repackage classes into the top-level. +-repackageclasses + +# Amount of optimization iterations, taken from an SO post +-optimizationpasses 5 +-mergeinterfacesaggressively + +# Broaden access modifiers to increase results during optimization +-allowaccessmodification diff --git a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libaliuhook.so b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libaliuhook.so index 42830e214..55bceb314 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libaliuhook.so and b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libaliuhook.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libc++_shared.so b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libc++_shared.so index 42dfca1f1..6a3e12a7a 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libc++_shared.so and b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/libc++_shared.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/liblsplant.so b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/liblsplant.so index a1e8df402..3ad9cd106 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/arm64-v8a/liblsplant.so and b/installer/android/app/src/main/assets/aliuhook/arm64-v8a/liblsplant.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libaliuhook.so b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libaliuhook.so index f0a1a7108..0b219e594 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libaliuhook.so and b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libaliuhook.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libc++_shared.so b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libc++_shared.so index d9c8eb28e..409935132 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libc++_shared.so and b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/libc++_shared.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/liblsplant.so b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/liblsplant.so index d2d30cda3..07655605c 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/liblsplant.so and b/installer/android/app/src/main/assets/aliuhook/armeabi-v7a/liblsplant.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/classes.dex b/installer/android/app/src/main/assets/aliuhook/classes.dex index d78a691fb..1948a840a 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/classes.dex and b/installer/android/app/src/main/assets/aliuhook/classes.dex differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86/libaliuhook.so b/installer/android/app/src/main/assets/aliuhook/x86/libaliuhook.so index b39cefda0..5f4d3154a 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86/libaliuhook.so and b/installer/android/app/src/main/assets/aliuhook/x86/libaliuhook.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86/libc++_shared.so b/installer/android/app/src/main/assets/aliuhook/x86/libc++_shared.so index b779944f6..cef4b64f0 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86/libc++_shared.so and b/installer/android/app/src/main/assets/aliuhook/x86/libc++_shared.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86/liblsplant.so b/installer/android/app/src/main/assets/aliuhook/x86/liblsplant.so index 3abbad080..90064c24c 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86/liblsplant.so and b/installer/android/app/src/main/assets/aliuhook/x86/liblsplant.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86_64/libaliuhook.so b/installer/android/app/src/main/assets/aliuhook/x86_64/libaliuhook.so index d2ee0f9bf..6ed90ffc5 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86_64/libaliuhook.so and b/installer/android/app/src/main/assets/aliuhook/x86_64/libaliuhook.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86_64/libc++_shared.so b/installer/android/app/src/main/assets/aliuhook/x86_64/libc++_shared.so index 4377d26ea..c0be4dbfd 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86_64/libc++_shared.so and b/installer/android/app/src/main/assets/aliuhook/x86_64/libc++_shared.so differ diff --git a/installer/android/app/src/main/assets/aliuhook/x86_64/liblsplant.so b/installer/android/app/src/main/assets/aliuhook/x86_64/liblsplant.so index 9fa3f1a41..e4a666f07 100644 Binary files a/installer/android/app/src/main/assets/aliuhook/x86_64/liblsplant.so and b/installer/android/app/src/main/assets/aliuhook/x86_64/liblsplant.so differ diff --git a/installer/android/build.gradle b/installer/android/build.gradle index b76700ce7..bc157bd1a 100644 --- a/installer/android/build.gradle +++ b/installer/android/build.gradle @@ -1,14 +1,3 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - } -} - allprojects { repositories { google() diff --git a/installer/android/gradle/wrapper/gradle-wrapper.properties b/installer/android/gradle/wrapper/gradle-wrapper.properties index 747b9fde5..dd9196dbc 100644 --- a/installer/android/gradle/wrapper/gradle-wrapper.properties +++ b/installer/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Apr 30 20:30:20 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/installer/android/settings.gradle b/installer/android/settings.gradle index 44e62bcf0..f0ecf8dbd 100644 --- a/installer/android/settings.gradle +++ b/installer/android/settings.gradle @@ -1,11 +1,24 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + repositories { + google() + mavenCentral() + } +} -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.2.2" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ':app' diff --git a/installer/lib/github.dart b/installer/lib/github.dart index 2dc480656..72e17d711 100644 --- a/installer/lib/github.dart +++ b/installer/lib/github.dart @@ -8,6 +8,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'utils/main.dart'; +import 'widgets/manager_dialog.dart'; import 'widgets/update_dialog.dart'; class Commit { @@ -16,13 +17,19 @@ class Commit { final CommitCommit commit; final String author; - Commit({ required this.htmlUrl, required this.sha, required this.commit, required this.author }); + Commit( + {required this.htmlUrl, + required this.sha, + required this.commit, + required this.author}); Commit.fromJson(Map json) - : htmlUrl = json['html_url'], - sha = json['sha'], - commit = CommitCommit(json['commit']['message']), - author = json['author'] == null ? json['commit']['author']['name'] : json['author']['login']; + : htmlUrl = json['html_url'], + sha = json['sha'], + commit = CommitCommit(json['commit']['message']), + author = json['author'] == null + ? json['commit']['author']['name'] + : json['author']['login']; } class CommitCommit { @@ -40,11 +47,11 @@ class Release { const Release(this.tag, this.name, this.body, this.assets); Release.fromJson(Map json) - : tag = json['tag_name'], - name = json['name'], - body = json['body'], - assets = (json['assets'] as Iterable) - .map((e) => Asset(e['name'], e['browser_download_url'])); + : tag = json['tag_name'], + name = json['name'], + body = json['body'], + assets = (json['assets'] as Iterable) + .map((e) => Asset(e['name'], e['browser_download_url'])); } class Asset { @@ -69,25 +76,39 @@ class GithubAPI with ChangeNotifier { void checkForUpdates() async { final release = await getLatestRelease(); if (release == null) return; + if (release.body.contains('Aliucord/Manager')) { + managerReleased = true; + return showDialog( + context: navigatorKey.currentContext!, + barrierDismissible: false, + builder: (context) => const ManagerDialog()); + } final currentVersion = await getVersionCode(); if (int.parse(release.tag.replaceAll('.', '')) <= currentVersion) return; final currentVersionName = await getVersionName(); - showDialog(context: navigatorKey.currentContext!, barrierDismissible: false, builder: (context) => UpdateDialog( - newVersion: release.tag, - currentVersion: currentVersionName, - message: release.body != '' ? release.body : release.name, - downloadUrl: release.assets.firstWhere((e) => e.name == 'Installer-release.apk').downloadUrl, - )); + showDialog( + context: navigatorKey.currentContext!, + barrierDismissible: false, + builder: (context) => UpdateDialog( + newVersion: release.tag, + currentVersion: currentVersionName, + message: release.body != '' ? release.body : release.name, + downloadUrl: release.assets + .firstWhere((e) => e.name == 'Installer-release.apk') + .downloadUrl, + )); } final Map> _commitsCache = {}; - Future> getCommits({ Map? params }) async { + Future> getCommits({Map? params}) async { final commitsUri = Uri.https(_apiHost, _commitsEndpoint, params); - if (_commitsCache.containsKey(commitsUri)) return _commitsCache[commitsUri]!; + if (_commitsCache.containsKey(commitsUri)) + return _commitsCache[commitsUri]!; try { final res = await dio.getUri(commitsUri); if (res.data is List) { - final commits = List>.from(res.data).map((e) => Commit.fromJson(e)); + final commits = List>.from(res.data) + .map((e) => Commit.fromJson(e)); _commitsCache[commitsUri] = commits; return commits; } @@ -99,7 +120,8 @@ class GithubAPI with ChangeNotifier { Future getCommit(String commit) async { try { - final res = await dio.getUri(Uri.https(_apiHost, '$_commitsEndpoint/$commit')); + final res = + await dio.getUri(Uri.https(_apiHost, '$_commitsEndpoint/$commit')); if (res.data is Map) return Commit.fromJson(res.data); } on DioException { // nop @@ -117,5 +139,6 @@ class GithubAPI with ChangeNotifier { return null; } - String getDownloadUrl(String ref, String file) => 'https://raw.githubusercontent.com/$_org/$_repo/$ref/$file'; + String getDownloadUrl(String ref, String file) => + 'https://raw.githubusercontent.com/$_org/$_repo/$ref/$file'; } diff --git a/installer/lib/utils/main.dart b/installer/lib/utils/main.dart index b11c38659..4ea1b887b 100644 --- a/installer/lib/utils/main.dart +++ b/installer/lib/utils/main.dart @@ -21,19 +21,23 @@ GithubAPI? githubAPI; final storageRoot = Directory('/storage/emulated/0'); late SharedPreferences prefs; final navigatorKey = GlobalKey(); +bool managerReleased = false; -Future pickFile(BuildContext context, String title, String ext) async => FilesystemPicker.open( - allowedExtensions: [ ext ], - context: context, - fileTileSelectMode: FileTileSelectMode.wholeTile, - fsType: FilesystemType.file, - rootDirectory: storageRoot, - title: title, -); +Future pickFile( + BuildContext context, String title, String ext) async => + FilesystemPicker.open( + allowedExtensions: [ext], + context: context, + fileTileSelectMode: FileTileSelectMode.wholeTile, + fsType: FilesystemType.file, + rootDirectory: storageRoot, + title: title, + ); void openUrl(String url) async => await AndroidIntent( - action: 'action_view', - data: url, -).launch(); + action: 'action_view', + data: url, + ).launch(); -bool isVersionSupported(int version, String supported) => version.toString().startsWith(supported); +bool isVersionSupported(int version, String supported) => + version.toString().startsWith(supported); diff --git a/installer/lib/utils/themes.dart b/installer/lib/utils/themes.dart index 4ca90df8d..5da564cfe 100644 --- a/installer/lib/utils/themes.dart +++ b/installer/lib/utils/themes.dart @@ -15,18 +15,26 @@ class Themes { static const primaryColorDark = Color(0xff009624); static final lightTheme = ThemeData( - appBarTheme: const AppBarTheme(systemOverlayStyle: SystemUiOverlayStyle.dark, iconTheme: IconThemeData(color: Colors.black)), + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.dark, + iconTheme: IconThemeData(color: Colors.black)), primaryColor: primaryColor, primaryColorLight: primaryColorLight, primaryColorDark: primaryColorDark, primaryTextTheme: const TextTheme( titleLarge: TextStyle(color: Colors.white), ), - colorScheme: const ColorScheme.light(primary: primaryColor, primaryContainer: primaryColorDark, secondary: primaryColor, onPrimary: Colors.white), + colorScheme: const ColorScheme.light( + primary: primaryColor, + primaryContainer: primaryColorDark, + secondary: primaryColor, + onPrimary: Colors.white), useMaterial3: true, ); static final darkTheme = ThemeData( - appBarTheme: const AppBarTheme(systemOverlayStyle: SystemUiOverlayStyle.light, iconTheme: IconThemeData(color: Colors.white)), + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle.light, + iconTheme: IconThemeData(color: Colors.white)), brightness: Brightness.dark, primaryColor: primaryColor, primaryColorLight: primaryColorLight, @@ -34,7 +42,11 @@ class Themes { primaryTextTheme: const TextTheme( titleLarge: TextStyle(color: Colors.white), ), - colorScheme: const ColorScheme.dark(primary: primaryColor, primaryContainer: primaryColorDark, secondary: primaryColor, onPrimary: Colors.white), + colorScheme: const ColorScheme.dark( + primary: primaryColor, + primaryContainer: primaryColorDark, + secondary: primaryColor, + onPrimary: Colors.white), useMaterial3: true, ); } @@ -51,16 +63,18 @@ class ThemeManager with ChangeNotifier { } ThemeData applyMonet(ThemeData theme, ColorScheme? dynamic) { - return dynamic != null ? theme.copyWith( - appBarTheme: theme.appBarTheme.copyWith( - iconTheme: IconThemeData(color: dynamic.secondary), - ), - colorScheme: dynamic, - primaryColor: null, - primaryColorLight: null, - primaryColorDark: null, - scaffoldBackgroundColor: dynamic.background, - ) : theme; + return dynamic != null + ? theme.copyWith( + appBarTheme: theme.appBarTheme.copyWith( + iconTheme: IconThemeData(color: dynamic.secondary), + ), + colorScheme: dynamic, + primaryColor: null, + primaryColorLight: null, + primaryColorDark: null, + scaffoldBackgroundColor: dynamic.surface, + ) + : theme; } } diff --git a/installer/lib/widgets/init_install.dart b/installer/lib/widgets/init_install.dart index a1d183974..a6a8a2e9a 100644 --- a/installer/lib/widgets/init_install.dart +++ b/installer/lib/widgets/init_install.dart @@ -8,9 +8,13 @@ import 'package:flutter/material.dart'; import '../pages/install.dart'; import '../utils/main.dart'; +import '../widgets/manager_dialog.dart'; void initInstall(BuildContext context, String commit, String supportedVersion) { - showDialog(context: context, builder: (context) => _InitInstallDialog(commit: commit, supportedVersion: supportedVersion)); + showDialog( + context: context, + builder: (context) => _InitInstallDialog( + commit: commit, supportedVersion: supportedVersion)); } enum _InstallOption { storage, installedApp, download } @@ -19,7 +23,9 @@ class _InitInstallDialog extends StatefulWidget { final String commit; final String supportedVersion; - const _InitInstallDialog({ Key? key, required this.commit, required this.supportedVersion }) : super(key: key); + const _InitInstallDialog( + {Key? key, required this.commit, required this.supportedVersion}) + : super(key: key); @override State<_InitInstallDialog> createState() => _InitInstallDialogState(); @@ -30,6 +36,7 @@ class _InitInstallDialogState extends State<_InitInstallDialog> { String? _apk; String? _package; _InstallOption? _option = _InstallOption.download; + bool _dismissedManagerDialog = false; bool _fetchingInstalled = false; void _selectFromInstalledApp(_InstallOption? value) async { @@ -38,80 +45,90 @@ class _InitInstallDialogState extends State<_InitInstallDialog> { final discordApps = await getInstalledDiscordApps(); _fetchingInstalled = false; // ignore: use_build_context_synchronously - showDialog(context: context, builder: (context) => AlertDialog( - title: const Text('Select apk from installed app'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: discordApps.map((app) => TextButton( - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - setState(() { - _option = value; - _download = false; - _apk = app.apkPath; - _package = app.packageName; - }); - }, - style: TextButton.styleFrom(padding: EdgeInsets.zero), - child: ListTile( - leading: app.icon == null ? null : Image.memory(app.icon!), - title: Text('${app.name!} (${app.packageName})'), - subtitle: Text( - '(ver. ${app.versionName} (${app.versionCode}))', - style: TextStyle(color: isVersionSupported(app.versionCode, widget.supportedVersion) ? Colors.green : Colors.red) - ), - ), - )).toList(), - ), - contentPadding: const EdgeInsets.symmetric(vertical: 10), - )); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Select apk from installed app'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: discordApps + .map((app) => TextButton( + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + setState(() { + _option = value; + _download = false; + _apk = app.apkPath; + _package = app.packageName; + }); + }, + style: TextButton.styleFrom(padding: EdgeInsets.zero), + child: ListTile( + leading: app.icon == null + ? null + : Image.memory(app.icon!), + title: Text('${app.name!} (${app.packageName})'), + subtitle: Text( + '(ver. ${app.versionName} (${app.versionCode}))', + style: TextStyle( + color: isVersionSupported(app.versionCode, + widget.supportedVersion) + ? Colors.green + : Colors.red)), + ), + )) + .toList(), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + )); } @override Widget build(BuildContext context) { + if (managerReleased && !_dismissedManagerDialog) { + return ManagerDialog( + onDismiss: () => setState(() => _dismissedManagerDialog = true)); + } + final children = [ - RadioListTile( - title: const Text('Download'), - value: _InstallOption.download, - groupValue: _option, - onChanged: (_InstallOption? value) => setState(() { - _option = value; - _download = true; - _apk = null; - _package = null; - }), - ), + RadioListTile( + title: const Text('Download'), + value: _InstallOption.download, + groupValue: _option, + onChanged: (_InstallOption? value) => setState(() { + _option = value; + _download = true; + _apk = null; + _package = null; + }), + ), ]; if (prefs.getBool('developer_mode') ?? false) { - children.add( - RadioListTile( - title: Text('From installed app ${_package == null ? '' : '($_package)'}'), - value: _InstallOption.installedApp, - groupValue: _option, - onChanged: _selectFromInstalledApp, - ) - ); - children.add( - RadioListTile( - title: Text('From storage ${_package == null && _apk != null - ? '(${_apk!.split('/').last})' - : ''}'), - value: _InstallOption.storage, - groupValue: _option, - onChanged: (_InstallOption? value) async { - final apk = await pickFile(context, 'Select Discord apk', '.apk'); - if (apk != null) { - setState(() { - _option = value; - _download = false; - _apk = apk; - _package = null; - }); - } - }, - ) - ); + children.add(RadioListTile( + title: + Text('From installed app ${_package == null ? '' : '($_package)'}'), + value: _InstallOption.installedApp, + groupValue: _option, + onChanged: _selectFromInstalledApp, + )); + children.add(RadioListTile( + title: Text( + 'From storage ${_package == null && _apk != null ? '(${_apk!.split('/').last})' : ''}'), + value: _InstallOption.storage, + groupValue: _option, + onChanged: (_InstallOption? value) async { + final apk = await pickFile(context, 'Select Discord apk', '.apk'); + if (apk != null) { + setState(() { + _option = value; + _download = false; + _apk = apk; + _package = null; + }); + } + }, + )); } return AlertDialog( @@ -121,10 +138,17 @@ class _InitInstallDialogState extends State<_InitInstallDialog> { TextButton( child: const Row( mainAxisSize: MainAxisSize.min, - children: [ Icon(Icons.archive_outlined), Text(' Install') ], + children: [Icon(Icons.archive_outlined), Text(' Install')], ), onPressed: () => Navigator.pushAndRemoveUntil( - context, MaterialPageRoute(builder: (context) => InstallPage(apk: _apk, commit: widget.commit, download: _download, supportedVersion: widget.supportedVersion)), (r) => false), + context, + MaterialPageRoute( + builder: (context) => InstallPage( + apk: _apk, + commit: widget.commit, + download: _download, + supportedVersion: widget.supportedVersion)), + (r) => false), ), ], ); diff --git a/installer/lib/widgets/manager_dialog.dart b/installer/lib/widgets/manager_dialog.dart new file mode 100644 index 000000000..2236f7c47 --- /dev/null +++ b/installer/lib/widgets/manager_dialog.dart @@ -0,0 +1,75 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2024 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../utils/main.dart'; + +class ManagerDialog extends StatefulWidget { + final void Function()? onDismiss; + + const ManagerDialog({this.onDismiss, super.key}); + + @override + State createState() => _ManagerDialogState(); +} + +class _ManagerDialogState extends State { + void Function()? _dismiss; + Timer? _timer; + int _seconds = 5; + + @override + void dispose() { + if (_timer?.isActive == true) _timer!.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _timer ??= Timer.periodic(const Duration(seconds: 1), (timer) { + if (_seconds == 1) { + timer.cancel(); + _dismiss = () { + if (widget.onDismiss == null) { + Navigator.of(context, rootNavigator: true).pop(); + } else { + widget.onDismiss!.call(); + } + }; + } + setState(() => _seconds--); + }); + return PopScope( + canPop: _seconds == 0, + child: AlertDialog( + title: const Text('Installer is no longer supported'), + content: const Text( + 'Please install new Aliucord Manager app for managing (installing, updating) Aliucord.\n\nAliucord Manager has improved user experience and support for new Android versions.\n\nAlso in the future some updates will require Aliucord apk update using Manager.\n\nUsing Installer may not install Aliucord correctly and may result with broken installation.'), + actions: [ + TextButton( + onPressed: _dismiss, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.cancel_outlined), + Text( + ' Dismiss (${_seconds == 0 ? 'not recommended' : _seconds})'), + ]), + ), + TextButton( + onPressed: () => + openUrl("https://github.com/Aliucord/Manager/releases"), + child: const Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.file_download), + Text(' Install Manager'), + ]), + ), + ], + ), + ); + } +} diff --git a/installer/pubspec.lock b/installer/pubspec.lock index 7b29537b7..637b2d3fc 100644 --- a/installer/pubspec.lock +++ b/installer/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: "direct main" description: name: android_intent_plus - sha256: "2c87d8330ba5deef5fe20e77f4d178190b3b24531dce08368030ab4be40a9d4e" + sha256: "007703c1b2cac7ca98add3336b98cffa4baa11d5133cc463293dba9daa39cdf6" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "5.1.0" async: dependency: transitive description: @@ -45,26 +45,34 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" dio: dependency: "direct main" description: name: dio - sha256: "9d6445da1caf8412070670c03c39ad5b12a78cc8c2361417b220905a2bcbdd2f" + sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.6.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d url: "https://pub.dev" source: hosted - version: "1.6.6" + version: "1.7.0" fake_async: dependency: transitive description: @@ -77,26 +85,26 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" filesystem_picker: dependency: "direct main" description: name: filesystem_picker - sha256: cf790e033b3e0c07b5bc9f71458b39f1f45017641aae508ffdfb86a59baa0c1d + sha256: cc2bfe7e5a4ce21afd5b1b03824c0e6e5386a981ed6cce7bda062b1af805cf62 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" flutter: dependency: "direct main" description: flutter @@ -106,10 +114,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_test: dependency: transitive description: flutter @@ -128,14 +136,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - js: + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "3.0.1" lints: dependency: transitive description: @@ -148,154 +172,154 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.2.10" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.4.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.8" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -305,26 +329,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -345,10 +369,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.7.2" typed_data: dependency: transitive description: @@ -365,22 +389,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - win32: + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + web: dependency: transitive description: - name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + name: web + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "1.0.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sdks: - dart: ">=3.0.0 <4.0.0" - flutter: ">=3.4.0-17.0.pre" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/installer/pubspec.yaml b/installer/pubspec.yaml index d8bef3d9b..dd5bf897c 100644 --- a/installer/pubspec.yaml +++ b/installer/pubspec.yaml @@ -14,16 +14,16 @@ dependencies: flutter: sdk: flutter - android_intent_plus: ^4.0.1 - collection: ^1.17.1 - dio: ^5.3.1 - filesystem_picker: ^3.1.0 - path_provider: ^2.0.15 - shared_preferences: ^2.2.0 - dynamic_color: ^1.6.6 + android_intent_plus: ^5.1.0 + collection: ^1.18.0 + dio: ^5.6.0 + filesystem_picker: ^4.1.0 + path_provider: 2.1.1 + shared_preferences: 2.2.2 + dynamic_color: ^1.7.0 dev_dependencies: - flutter_lints: ^2.0.2 + flutter_lints: ^2.0.3 # The following section is specific to Flutter. flutter: diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 000000000..28f2b2f18 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,27 @@ +## smali patches + +Certain things are too difficult to patch via hooking, and this has become necessary in order to continue backporting features from RNA. + +These patches are organized in a way that + +1. Each patch can only be applied to ONE class +2. Each patch is based on the output of configured [`google/smali@3.0.7`](https://github.com/google/smali). **APKTOOL WILL NOT WORK.** +3. Each patch's path is the the qualified class name + `.patch`, relative from `src/` + - For example, patching the class `com.discord.BuildConfig` would have a patch at `src/com/discord/BuildConfig.patch`. The before/after file name + header in the patch does not matter. +4. Each patch is a unified diff generated by the `diff` util (not git). + +These patches are then bundled and published to be downloaded by the [Aliucord Manager](https://github.com/Aliucord/Manager) which applies them before +installing Aliucord. + +## Tooling + +1. Ensure you have diffutils (`diff`/`patch`) installed (on windows: [via chocolatey](https://community.chocolatey.org/packages/diffutils)) +2. Run the `:patches:disassembleWithPatches` task in order to disassemble and apply any existing patches in `./src` +3. Perform any additional modifications necessary to the `./smali` directory +4. Run the `:patches:test` task in order to check that the smali still successfully reassembles +5. Run the `:patches:writePatches` task in order to generate and write the patches back to `./src` +6. Update the patches version in `build.gradle.kts` if necessary +7. Commit changes to git + +Any changes made to `./src` after having run `:patches:disassembleWithPatches` will be discarded when generating patches! diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts new file mode 100644 index 000000000..de2462675 --- /dev/null +++ b/patches/build.gradle.kts @@ -0,0 +1,210 @@ +import com.android.build.gradle.LibraryExtension +import java.io.ByteArrayOutputStream + +version = "1.0.0" + +// Make dependency configuration for build tools +val buildTools: Configuration by configurations.creating + +repositories { + mavenCentral() + google() +} + +dependencies { + val smaliVersion = "3.0.7" + buildTools("com.android.tools.smali:smali:$smaliVersion") + buildTools("com.android.tools.smali:smali-baksmali:$smaliVersion") +} + +val patchesDir = projectDir.resolve("src") +val smaliDir = projectDir.resolve("smali") +val smaliOriginalDir = buildDir.resolve("smali_original") +val discordApk = project.gradle.gradleUserHomeDir + .resolve("caches/aliucord/discord/discord-${libs.discord.get().version}.apk") + +// --- Public tasks --- // + +task("package") { + group = "aliucord" + archiveFileName.set("patches.zip") + destinationDirectory.set(buildDir) + isReproducibleFileOrder = true + isPreserveFileTimestamps = false + + from(patchesDir) + include(".gitkeep") + include("**/*.patch") +} + +task("deployWithAdb") { + group = "aliucord" + dependsOn("package") + + val patchesPath = buildDir.resolve("patches.zip").absolutePath + val remotePatchesDir = "/storage/emulated/0/Android/data/com.aliucord.manager/cache/patches" + + doLast { + val android = project(":Aliucord").extensions + .getByName("android") + + exec { commandLine(android.adbExecutable, "shell", "mkdir", "-p", remotePatchesDir) } + exec { commandLine(android.adbExecutable, "push", patchesPath, "$remotePatchesDir/$version.custom.zip") } + } +} + +task("disassembleWithPatches") { + group = "aliucord" + dependsOn("disassembleInternal", "copyDisassembled", "applyPatches") +} + +task("test") { + group = "aliucord" + dependsOn("assemble") +} + +task("writePatches") { + group = "aliucord" + mustRunAfter("applyPatches") + + doFirst { + if (!smaliDir.exists() || !smaliOriginalDir.exists()) { + error("Smali directory does not exist! Run the disassembleWithPatches task") + } + + val stdout = ByteArrayOutputStream() + val result = exec { + isIgnoreExitValue = true + standardOutput = stdout + errorOutput = System.err + executable = "diff" + args = listOf( + "--unified", + "--minimal", + "--new-file", + "--recursive", + "--strip-trailing-cr", + "--show-function-line=.method", + smaliOriginalDir.toRelativeString(projectDir), + smaliDir.toRelativeString(projectDir), + ) + } + + // diff returns 1 if changes are present + if (result.exitValue !in 0..1) result.assertNormalExitValue() + + val diffs = stdout + .toString() // Convert bytes to string + .split("^diff --unified.+?\\R".toRegex(RegexOption.MULTILINE)) // Split by file diff header + .filter(String::isNotBlank) + + patchesDir.deleteRecursively() + patchesDir.mkdirs() + patchesDir.resolve(".gitkeep").createNewFile() + + val classNameRegex = """^\+\+\+ \.?[\/]?smali[\/](.+?)\.smali\t""".toRegex(RegexOption.MULTILINE) + for (diff in diffs) { + val (className) = classNameRegex.find(diff)?.destructured + ?: error("failed to parse diff:\n$diff") + + logger.lifecycle("Writing patch for class $className") + + File(patchesDir, "$className.patch") + .apply { parentFile.mkdirs() } + .writeText(diff) + } + } +} + +task("clean") { + delete(buildDir) + delete(smaliDir) +} + +// --- Internal tasks --- // + +task("disassembleInternal") { + classpath = buildTools + jvmArgs = listOf("-Xmx2G") + standardOutput = System.out + errorOutput = System.err + + outputs.upToDateWhen { + smaliOriginalDir.exists() + } + + mainClass.set("com.android.tools.smali.baksmali.Main") + args = listOf( + "disassemble", + "--use-locals", + "--output", smaliOriginalDir.absolutePath, + discordApk.absolutePath, + ) +} + +task("copyDisassembled") { + mustRunAfter("disassembleInternal") + doFirst { + delete(smaliDir) + } + + from(smaliOriginalDir) + destinationDir = smaliDir +} + +task("applyPatches") { + mustRunAfter("copyDisassembled") + doLast { + val patchFiles = fileTree(patchesDir).filter { it.name.endsWith(".patch") } + + for (patchFile in patchFiles) { + val className = patchFile + .toRelativeString(patchesDir) + .removeSuffix(".patch") + + logger.lifecycle("Applying smali patches to class $className") + + exec { + standardOutput = System.out + errorOutput = System.err + executable = "patch" + args = listOf( + "--verbose", + "--forward", + "--unified", + smaliDir.resolve("$className.smali").absolutePath, + patchFile.absolutePath, + ) + } + } + } +} + +task("assemble") { + val outputDex = buildDir.resolve("patched.dex").absolutePath + + standardOutput = System.out + errorOutput = System.err + classpath = buildTools + jvmArgs = listOf("-Xmx2G") + mainClass.set("com.android.tools.smali.smali.Main") + args = listOf( + "assemble", + "--verbose", + "--output", outputDex, + smaliDir.absolutePath, + ) + + mustRunAfter("applyPatches") + doFirst { + delete(outputDex) + + if (!smaliDir.exists()) { + error("Smali directory does not exist! Run the disassembleWithPatches task") + } + } + + doLast { + logger.lifecycle("Successfully reassembled dex: {}", outputDex) + } +} diff --git a/patches/src/.gitkeep b/patches/src/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/settings.gradle.kts b/settings.gradle.kts index b19ba05a8..b574d5faa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,5 @@ pluginManagement { include(":Aliucord") include(":Injector") +include(":patches") rootProject.name = "Aliucord"