diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml
index 87c8e4fb67..73017f1b61 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue.yml
+++ b/.github/ISSUE_TEMPLATE/bug-issue.yml
@@ -1,120 +1,61 @@
-name: 🐞 Bug report
-description: Report a very clearly broken issue.
-title: 'bug:
'
-labels: [bug]
-body:
- - type: markdown
- attributes:
- value: |
- # ReVanced Manager bug report
-
- Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug).
-
- - type: dropdown
- attributes:
- label: Type
- options:
- - Error while running the manager
- - Error at runtime
- - Cosmetic
- - Other
- validations:
- required: true
- - type: textarea
- attributes:
- label: Bug description
- description: How did you find the bug? Any additional details that might help?
- validations:
- required: true
- - type: textarea
- attributes:
- label: Steps to reproduce
- description: Add the steps to reproduce this bug, including your environment.
- placeholder: Step 1. Download some files. Step 2. ...
- validations:
- required: true
- - type: textarea
- attributes:
- label: Android version
- description: Android version used.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Manager version
- description: Manager version used.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Target package name
- description: App you tried to patch.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Target package version.
- description: Version of the app you tried to patch.
- validations:
- required: true
- - type: dropdown
- attributes:
- label: Installation type
- options:
- - Non-root
- - Root
- validations:
- required: true
- - type: textarea
- attributes:
- label: Patches selected.
- description: Patches you selected for the app.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Device logs (exported using Manager settings).
- description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
- render: shell
- validations:
- required: true
- - type: textarea
- attributes:
- label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching].
- description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks.
- render: shell
- validations:
- required: false
- - type: textarea
- attributes:
- label: Screenshots or video
- description: Add screenshots or videos that show the bug here.
- placeholder: Drag and drop the screenshots/videos into this box.
- validations:
- required: false
- - type: textarea
- attributes:
- label: Solution
- description: If applicable, add a possible solution.
- validations:
- required: false
- - type: textarea
- attributes:
- label: Additional context
- description: Add additional context here.
- validations:
- required: false
- - type: checkboxes
- id: acknowledgments
- attributes:
- label: Acknowledgments
- description: Your issue will be closed if you haven't done these steps.
- options:
- - label: I have searched the existing issues; this is new and no duplicate or related to another open issue.
- required: true
- - label: I have written a short but informative title.
- required: true
- - label: I properly filled out all of the requested information in this issue.
- required: true
- - label: The issue is solely related to ReVanced Manager and not caused by patches.
- required: true
+name: 🐞 Bug report
+description: Create a new bug report.
+title: 'bug: '
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # ReVanced Manager bug report
+
+ Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
+ - type: textarea
+ attributes:
+ label: Bug description
+ description: |
+ - Describe your bug in detail
+ - Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...)
+ - Add images and videos if possible
+ - List selected patches if applicable
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Version of ReVanced Manager and version & name of application you tried to patch
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: Installation type
+ options:
+ - Non-root
+ - Root
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Device logs
+ description: Export logs in ReVanced Manager settings.
+ render: shell
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Patcher logs
+ description: Export logs in "Patcher" screen.
+ render: shell
+ validations:
+ required: false
+ - type: checkboxes
+ attributes:
+ label: Acknowledgements
+ description: Your issue will be closed if you don't follow the checklist below!
+ options:
+ - label: This request is not a duplicate of an existing issue.
+ required: true
+ - label: I have chosen an appropriate title.
+ required: true
+ - label: All requested information has been provided properly.
+ required: true
+ - label: The issue is solely related to the ReVanced Manager
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature-issue.yml b/.github/ISSUE_TEMPLATE/feature-issue.yml
index 8df4eab2d7..ca76ef0020 100644
--- a/.github/ISSUE_TEMPLATE/feature-issue.yml
+++ b/.github/ISSUE_TEMPLATE/feature-issue.yml
@@ -1,52 +1,42 @@
-name: ⭐ Feature request
-description: Create a detailed feature request.
-title: 'feat: '
-labels: [feature-request]
-body:
- - type: dropdown
- attributes:
- label: Type
- options:
- - Functionality
- - Cosmetic
- - Other
- validations:
- required: true
- - type: textarea
- attributes:
- label: Issue
- description: What is the current problem. Why does it require a feature request?
- validations:
- required: true
- - type: textarea
- attributes:
- label: Feature
- description: Describe your feature in detail. How does it solve the issue?
- validations:
- required: true
- - type: textarea
- attributes:
- label: Motivation
- description: Why should your feature should be considered?
- validations:
- required: true
- - type: textarea
- attributes:
- label: Additional context
- description: Add additional context here.
- validations:
- required: false
- - type: checkboxes
- id: acknowledgements
- attributes:
- label: Acknowledgements
- description: Your issue will be closed if you haven't done these steps.
- options:
- - label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
- required: true
- - label: I have written a short but informative title.
- required: true
- - label: I filled out all of the requested information in this issue properly.
- required: true
- - label: The issue is related solely to the ReVanced Manager
- required: true
+name: ⭐ Feature request
+description: Create a new feature request.
+title: 'feat: '
+labels: [feature request]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ # ReVanced Manager feature request
+
+ Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one.
+ - type: textarea
+ attributes:
+ label: Feature description
+ description: Describe your feature in detail.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Motivation
+ description: Explain why the lack of it is a problem.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: In case there is something else you want to add.
+ validations:
+ required: false
+ - type: checkboxes
+ attributes:
+ label: Acknowledgements
+ description: Your issue will be closed if you don't follow the checklist below!
+ options:
+ - label: This request is not a duplicate of an existing issue.
+ required: true
+ - label: I have chosen an appropriate title.
+ required: true
+ - label: All requested information has been provided properly.
+ required: true
+ - label: The issue is solely related to the ReVanced Manager
+ required: true
diff --git a/.github/config.yaml b/.github/config.yaml
index 650941e517..aaeba6e2da 100644
--- a/.github/config.yaml
+++ b/.github/config.yaml
@@ -1,2 +1,2 @@
firstPRMergeComment: >
- Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
\ No newline at end of file
+ ❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml
deleted file mode 100644
index e3f0f0638b..0000000000
--- a/.github/workflows/analyze.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Analyze Code
-
-on:
- push:
- branches: [ "dev" ]
- paths:
- - "**.dart"
- - ".github/workflows/analyze.yml"
- pull_request:
- branches: [ "main", "dev" ]
- types:
- - opened
- - reopened
- - synchronize
- - ready_for_review
- paths:
- - "**.dart"
- - ".github/workflows/analyze.yml"
-
-jobs:
- build:
- name: "Static analysis & format check"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - name: Setup Flutter
- uses: subosito/flutter-action@v2
- with:
- channel: 'stable'
- cache: true
- - name: Install Flutter dependencies
- run: flutter pub get
- - name: Generate files with Builder
- run: flutter packages pub run build_runner build --delete-conflicting-outputs
- - name: Analyze code
- uses: ValentinVignal/action-dart-analyze@v0.15
- with:
- fail-on: warning
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
index 0bb9bdb11f..4ca4a56580 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/pr-build.yml
@@ -1,45 +1,44 @@
-name: PR Build
+name: Build pull request
on:
pull_request:
- paths:
+ paths:
- ".github/workflows/pr-build.yml"
- - "android/**"
- - "assets/**"
- - "lib/**"
-
+ - "app/**"
+ - "gradle/**"
+ - "*.properties"
+ - ".kts"
+
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
- with:
- # Make sure the release step uses its own credentials:
- # https://github.com/cycjimmy/semantic-release-action#private-packages
- persist-credentials: false
- fetch-depth: 0
- - name: Setup JDK
- uses: actions/setup-java@v3
- with:
- java-version: '11'
- distribution: 'zulu'
- - name: Setup Flutter
- uses: subosito/flutter-action@v2
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
with:
- channel: 'stable'
- cache: true
- - name: Install Flutter dependencies
- run: flutter pub get
- - name: Generate files with Builder
- run: flutter packages pub run build_runner build --delete-conflicting-outputs
- - name: Build with Flutter
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: flutter build apk --debug
+ run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug
+
+ - name: Set env
+ run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
+
+ - name: Add hash to APK
+ run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
+
- name: Upload build
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: revanced-manager
- path: build/app/outputs/flutter-apk/app-debug.apk
+ path: revanced-manager-${{ env.COMMIT_HASH }}.apk
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index 4b414eb7ef..4b01fe6308 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -1,4 +1,4 @@
-name: "Release Build"
+name: Release Build
on:
push:
@@ -6,45 +6,52 @@ on:
- "v*"
jobs:
- release:
+ build:
+ name: Build
runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ attestations: write
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- - name: Set up JDK 11
- uses: actions/setup-java@v3
- with:
- java-version: "11"
- distribution: "zulu"
- - uses: subosito/flutter-action@v2
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
with:
- channel: "stable"
- - name: Set up Flutter
- run: flutter pub get
- - name: Generate files with Builder
- run: flutter packages pub run build_runner build --delete-conflicting-outputs
- - name: Build with Flutter
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
- SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
- SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
- run: flutter build apk
+ run: ./gradlew assembleRelease --no-daemon
+
- name: Sign APK
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
- releaseDir: build/app/outputs/apk/release
+ releaseDir: ./app/build/outputs/apk/release/
signingKey: ${{ secrets.SIGNING_KEYSTORE }}
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
+
- name: Add version to APK
- run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk
+ run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
+
+ - name: Generate artifact attestation
+ uses: actions/attest-build-provenance@v1
+ with:
+ subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk
+
- name: Publish release APK
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
- files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
\ No newline at end of file
+ files: revanced-manager-${{ env.RELEASE_VERSION }}.apk
diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml
index 77097e2fe6..541a7aa5b5 100644
--- a/.github/workflows/update-documentation.yml
+++ b/.github/workflows/update-documentation.yml
@@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- - uses: peter-evans/repository-dispatch@v2
+ - uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation
diff --git a/.gitignore b/.gitignore
index 2f30200232..c0af92ebad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,144 +1,12 @@
-# Miscellaneous
-*.class
-*.lock
-*.log
-*.pyc
-*.swp
-.DS_Store
-.atom/
-.buildlog/
-.history
-.svn/
-
-# IntelliJ related
*.iml
-*.ipr
-*.iws
-.idea/
-
-# Visual Studio Code related
-.classpath
-.project
-.settings/
-
-# Flutter repo-specific
-/bin/cache/
-/bin/mingit/
-/dev/benchmarks/mega_gallery/
-/dev/bots/.recipe_deps
-/dev/bots/android_tools/
-/dev/docs/doc/
-/dev/docs/flutter.docs.zip
-/dev/docs/lib/
-/dev/docs/pubspec.yaml
-/dev/integration_tests/**/xcuserdata
-/dev/integration_tests/**/Pods
-/packages/flutter/coverage/
-version
-
-# packages file containing multi-root paths
-.packages.generated
-
-# Flutter/Dart/Pub related
-**/doc/api/
-**/*.g.dart
-**/*.locator.dart
-**/*.router.dart
-.dart_tool/
-.flutter-plugins
-.flutter-plugins-dependencies
-**/generated_plugin_registrant.dart
-.packages
-.pub-cache/
-.pub/
-build/
-flutter_*.png
-linked_*.ds
-unlinked.ds
-unlinked_spec.ds
-
-# Android related
-**/android/**/gradle-wrapper.jar
-**/android/.gradle
-**/android/captures/
-**/android/gradlew
-**/android/gradlew.bat
-**/android/local.properties
-**/android/**/GeneratedPluginRegistrant.java
-**/android/key.properties
-*.jks
-
-# iOS/XCode related
-**/ios/**/*.mode1v3
-**/ios/**/*.mode2v3
-**/ios/**/*.moved-aside
-**/ios/**/*.pbxuser
-**/ios/**/*.perspectivev3
-**/ios/**/*sync/
-**/ios/**/.sconsign.dblite
-**/ios/**/.tags*
-**/ios/**/.vagrant/
-**/ios/**/DerivedData/
-**/ios/**/Icon?
-**/ios/**/Pods/
-**/ios/**/.symlinks/
-**/ios/**/profile
-**/ios/**/xcuserdata
-**/ios/.generated/
-**/ios/Flutter/.last_build_id
-**/ios/Flutter/App.framework
-**/ios/Flutter/Flutter.framework
-**/ios/Flutter/Flutter.podspec
-**/ios/Flutter/Generated.xcconfig
-**/ios/Flutter/app.flx
-**/ios/Flutter/app.zip
-**/ios/Flutter/flutter_assets/
-**/ios/Flutter/flutter_export_environment.sh
-**/ios/ServiceDefinitions.json
-**/ios/Runner/GeneratedPluginRegistrant.*
-
-# macOS related
-**/macos/Flutter/GeneratedPluginRegistrant.swift
-**/macos/Flutter/Flutter-Debug.xcconfig
-**/macos/Flutter/Flutter-Release.xcconfig
-**/macos/Flutter/Flutter-Profile.xcconfig
-
-# Windows related
-**/windows/flutter/ephemeral/
-**/windows/**/*.suo
-**/windows/**/*.user
-**/windows/**/*.userosscache
-**/windows/**/*.sln.docstates
-**/windows/x64/
-**/windows/x86/
-**/windows/**/*.[Cc]ache
-**/windows/**/!*.[Cc]ache/
-
-# Web related
-lib/generated_plugin_registrant.dart
-
-# Coverage
-coverage/
-
-# Symbolication related
-app.*.symbols
-
-# Obfuscation related
-app.*.map.json
-
-# Exceptions to above rules.
-!**/ios/**/default.mode1v3
-!**/ios/**/default.mode2v3
-!**/ios/**/default.pbxuser
-!**/ios/**/default.perspectivev3
-!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
-!/dev/ci/**/Gemfile.lock
-
-# Firebase related
-.firebase
-
-# Dependency directories
-node_modules/
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
-# FVM
-.fvm
\ No newline at end of file
+.kotlin/
diff --git a/.metadata b/.metadata
deleted file mode 100644
index e7c1001054..0000000000
--- a/.metadata
+++ /dev/null
@@ -1,45 +0,0 @@
-# This file tracks properties of this Flutter project.
-# Used by Flutter tool to assess capabilities and perform upgrades etc.
-#
-# This file should be version controlled.
-
-version:
- revision: 85684f9300908116a78138ea4c6036c35c9a1236
- channel: stable
-
-project_type: app
-
-# Tracks metadata for the flutter migrate command
-migration:
- platforms:
- - platform: root
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: android
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: ios
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: linux
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: macos
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: web
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- - platform: windows
- create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
-
- # User provided section
-
- # List of Local paths (relative to this file) that should be
- # ignored by the migrate tool.
- #
- # Files that are not part of the templates will be ignored by default.
- unmanaged_files:
- - 'lib/main.dart'
- - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/.releaserc b/.releaserc
deleted file mode 100644
index a10173d648..0000000000
--- a/.releaserc
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- "branches": [
- "main",
- {
- "name": "dev",
- "prerelease": true
- }
- ],
- "plugins": [
- "semantic-release-export-data",
- "@semantic-release/commit-analyzer",
- [
- "@semantic-release/release-notes-generator",
- {
- "presetConfig": {
- "types": [
- {
- "type": "build",
- "section": "Dependency Updates"
- },
- {
- "type": "chore",
- "section": "Other Changes",
- "hidden": false
- },
- {
- "type": "perf",
- "section": "Performance Improvements",
- "hidden": false
- },
- {
- "type": "refactor",
- "section": "Code Improvements",
- "hidden": false
- }
- ]
- }
- }
- ],
- "@semantic-release/changelog",
- "semantic-release-flutter-plugin",
- [
- "@semantic-release/git",
- {
- "assets": [
- "CHANGELOG.md",
- "pubspec.yaml"
- ]
- }
- ],
- [
- "@semantic-release/github",
- {
- "assets": [
- {
- "path": "build/app/outputs/apk/release/revanced-manager-*.apk"
- }
- ],
- "successComment": false
- }
- ],
- [
- "@saithodev/semantic-release-backmerge",
- {
- "backmergeBranches": [
- {
- "from": "main",
- "to": "dev"
- }
- ],
- "clearWorkspace": true
- }
- ]
- ]
-}
diff --git a/.run/main.dart.run.xml b/.run/main.dart.run.xml
deleted file mode 100644
index 4767aff814..0000000000
--- a/.run/main.dart.run.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
deleted file mode 100644
index a09c28f5e6..0000000000
--- a/.vscode/tasks.json
+++ /dev/null
@@ -1,91 +0,0 @@
-{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Generate (Builder)",
- "type": "shell",
- "command": "flutter packages pub run build_runner build --delete-conflicting-outputs",
- "problemMatcher": []
- },
- {
- "label": "Build (Android)",
- "type": "shell",
- "command": "flutter build apk",
- "problemMatcher": [],
- "group": {
- "kind": "build",
- "isDefault": true
- }
- },
- {
- "label": "Install (Android)",
- "type": "shell",
- "command": "adb install build\\app\\outputs\\flutter-apk\\app-release.apk",
- "problemMatcher": []
- },
- {
- "label": "Clean (Flutter)",
- "type": "shell",
- "command": "flutter clean && flutter pub get",
- "problemMatcher": []
- },
- {
- "label": "Clean (Builder)",
- "type": "shell",
- "command": "flutter packages pub run build_runner clean",
- "problemMatcher": []
- },
- {
- "label": "Build all (Android)",
- "dependsOrder": "sequence",
- "dependsOn": [
- "Generate (Builder)",
- "Build (Android)"
- ],
- "problemMatcher": []
- },
- {
- "label": "Clean all",
- "dependsOrder": "sequence",
- "dependsOn": [
- "Clean (Flutter)",
- "Clean (Builder)"
- ],
- "problemMatcher": []
- },
- {
- "label": "Clean all & Build all (Android)",
- "dependsOrder": "sequence",
- "dependsOn": [
- "Clean all",
- "Build all (Android)"
- ],
- "problemMatcher": []
- },
- {
- "label": "Clean all & Install (Android)",
- "dependsOrder": "sequence",
- "dependsOn": [
- "Clean all",
- "Build all (Android)",
- "Install (Android)",
- ],
- "problemMatcher": []
- },
- {
- "label": "Build & Install (Android)",
- "dependsOrder": "sequence",
- "dependsOn": [
- "Build (Android)",
- "Install (Android)"
- ],
- "problemMatcher": []
- },
- {
- "label": "Validate translations",
- "type": "shell",
- "command": "flutter pub run flutter_i18n diff en.json pt.json",
- "problemMatcher": []
- }
- ]
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 8b13789179..0000000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/README.md b/README.md
index 07ed4a791f..c742bcab29 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,55 @@
-# 💊 ReVanced Manager
+# ReVanced Manager (Compose Rewrite)
-The official ReVanced Manager based on Flutter.
+[![GitHub license](https://img.shields.io/github/license/revanced/revanced-manager)](../../blob/main/LICENSE)
+[![GitHub last commit](https://img.shields.io/github/last-commit/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
+[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev)
+
+_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose.
+
+## Design system
+
+In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You.
+
+Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel.
+
+### Why Material 3?
+
+* **Consistent design language**
+* **Improved accessibility**
+* **Better user experience**
+
+By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager.
+
+## Technology stack
+
+* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java.
+* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework.
+
+## Why Kotlin and Compose?
+
+* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes.
+* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase.
+* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience.
+* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces.
## 🔽 Download
-To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
+
+You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases)
## 📝 Prerequisites
-1. Android 8 or higher
-2. Does not work on some armv7 devices
-## 🔴 Issues
-For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
+For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md)
-## 💭 Discussion
-If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
+## 🔴 Issues
+For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
## 🌐 Translation
+
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
-If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
+We're accepting translations on [Crowdin](https://translate.revanced.app)
## 🛠️ Building Manager from source
-1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
-2. Clone the repository locally
-3. Add your github token in gradle.properties like [this](/docs/4_building.md)
-4. Open the project in terminal
-5. Run `flutter pub get` in terminal
-6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
-7. To build release apk run `flutter build apk`
+
+For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md)
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..15e3660da6
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Continuing the legacy of Vanced
+
+
+# 🔒 Security Policy
+
+This document describes how to report security vulnerabilities for ReVanced Manager.
+
+## 🚨 Reporting a Vulnerability
+
+Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
+
+If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
+
+### ⏳ Supported Versions
+
+| Version | Branch | Supported |
+| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
+| ![Latest stable release](https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge "Latest stable release") | main | :white_check_mark: |
+| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | dev | :white_check_mark: |
+| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | compose-dev | :white_check_mark: |
diff --git a/analysis_options.yaml b/analysis_options.yaml
deleted file mode 100644
index 1c02c24c3a..0000000000
--- a/analysis_options.yaml
+++ /dev/null
@@ -1,163 +0,0 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
-include: package:flutter_lints/flutter.yaml
-
-analyzer:
- exclude:
- - lib/app/app.locator.dart
- - lib/app/app.router.dart
- - lib/models/patch.g.dart
- - lib/models/patched_application.g.dart
-
-linter:
- rules:
- - always_declare_return_types
- - require_trailing_commas
- - always_put_control_body_on_new_line
- - always_require_non_null_named_parameters
- - always_use_package_imports # we do this commonly
- - annotate_overrides
- - avoid_bool_literals_in_conditional_expressions
- - avoid_double_and_int_checks
- - avoid_empty_else
- - avoid_equals_and_hash_code_on_mutable_classes
- - avoid_escaping_inner_quotes
- - avoid_field_initializers_in_const_classes
- - avoid_function_literals_in_foreach_calls
- - avoid_implementing_value_types
- - avoid_init_to_null
- - avoid_js_rounded_ints
- - avoid_null_checks_in_equality_operators
- - avoid_print
- - avoid_redundant_argument_values
- - avoid_relative_lib_imports
- - avoid_renaming_method_parameters
- - avoid_return_types_on_setters
- - avoid_returning_null
- - avoid_returning_null_for_future
- - avoid_returning_null_for_void
- - avoid_setters_without_getters
- - avoid_shadowing_type_parameters
- - avoid_single_cascade_in_expression_statements
- - avoid_type_to_string
- - avoid_types_as_parameter_names
- - avoid_unnecessary_containers
- - avoid_void_async
- - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
- - await_only_futures
- - camel_case_extensions
- - camel_case_types
- - cancel_subscriptions
- - cast_nullable_to_non_nullable
- - close_sinks # not reliable enough
- - control_flow_in_finally
- - curly_braces_in_flow_control_structures
- - depend_on_referenced_packages
- - deprecated_consistency
- - directives_ordering
- - empty_catches
- - empty_constructor_bodies
- - empty_statements
- - eol_at_end_of_file
- - exhaustive_cases
- - file_names
- - flutter_style_todos
- - hash_and_equals
- - implementation_imports
- - collection_methods_unrelated_type
- - leading_newlines_in_multiline_strings
- - library_names
- - library_prefixes
- - library_private_types_in_public_api
- - missing_whitespace_between_adjacent_strings
- - no_adjacent_strings_in_list
- - no_duplicate_case_values
- - no_logic_in_create_state
- - non_constant_identifier_names
- - noop_primitive_operations
- - null_check_on_nullable_type_parameter
- - null_closures
- - overridden_fields
- - package_api_docs
- - package_names
- - package_prefixed_library_names
- - prefer_adjacent_string_concatenation
- - prefer_asserts_in_initializer_lists
- - prefer_collection_literals
- - prefer_conditional_assignment
- - prefer_const_constructors
- - prefer_const_constructors_in_immutables
- - prefer_const_declarations
- - prefer_const_literals_to_create_immutables
- - prefer_contains
- - prefer_final_fields
- - prefer_final_in_for_each
- - prefer_final_locals
- - prefer_for_elements_to_map_fromIterable
- - prefer_foreach
- - prefer_function_declarations_over_variables
- - prefer_generic_function_type_aliases
- - prefer_if_elements_to_conditional_expressions
- - prefer_if_null_operators
- - prefer_initializing_formals
- - prefer_inlined_adds
- - prefer_interpolation_to_compose_strings
- - prefer_is_empty
- - prefer_is_not_empty
- - prefer_is_not_operator
- - prefer_iterable_whereType
- - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
- - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
- - prefer_null_aware_operators
- - prefer_single_quotes
- - prefer_spread_collections
- - prefer_typing_uninitialized_variables
- - prefer_void_to_null
- - provide_deprecation_message
- - recursive_getters
- - sized_box_for_whitespace
- - slash_for_doc_comments
- - sort_child_properties_last
- - sort_constructors_first
- - sort_unnamed_constructors_first
- - test_types_in_equals
- - throw_in_finally
- - tighten_type_of_initializing_formals
- - type_init_formals
- - unnecessary_brace_in_string_interps
- - unnecessary_const
- - unnecessary_getters_setters
- - unnecessary_new
- - unnecessary_null_aware_assignments
- - unnecessary_null_checks
- - unnecessary_null_in_if_null_operators
- - unnecessary_nullable_for_final_variable_declarations
- - unnecessary_overrides
- - unnecessary_parenthesis
- - unnecessary_statements
- - unnecessary_string_escapes
- - unnecessary_string_interpolations
- - unnecessary_this
- - unrelated_type_equality_checks
- - unsafe_html
- - use_build_context_synchronously
- - use_full_hex_values_for_flutter_colors
- - use_function_type_syntax_for_parameters
- - use_if_null_to_convert_nulls_to_bools
- - use_is_even_rather_than_modulo
- - use_key_in_widget_constructors
- - use_late_for_private_fields_and_variables
- - use_named_constants
- - use_raw_strings
- - use_rethrow_when_possible
- - use_setters_to_change_properties
- - use_test_throws_matchers
- - valid_regexps
- - void_checks
diff --git a/android/.gitignore b/android/.gitignore
deleted file mode 100644
index 6f568019d3..0000000000
--- a/android/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-gradle-wrapper.jar
-/.gradle
-/captures/
-/gradlew
-/gradlew.bat
-/local.properties
-GeneratedPluginRegistrant.java
-
-# Remember to never publicly share your keystore.
-# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
-key.properties
-**/*.keystore
-**/*.jks
diff --git a/android/Gemfile b/android/Gemfile
deleted file mode 100644
index 7a118b49be..0000000000
--- a/android/Gemfile
+++ /dev/null
@@ -1,3 +0,0 @@
-source "https://rubygems.org"
-
-gem "fastlane"
diff --git a/android/app/build.gradle b/android/app/build.gradle
deleted file mode 100644
index 8663736a80..0000000000
--- a/android/app/build.gradle
+++ /dev/null
@@ -1,92 +0,0 @@
-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'
-}
-
-def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
- flutterVersionName = '1.0'
-}
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
-android {
- compileSdkVersion flutter.compileSdkVersion
- ndkVersion flutter.ndkVersion
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
- }
-
- kotlinOptions {
- jvmTarget = '11'
- }
-
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
- }
-
- defaultConfig {
- applicationId "app.revanced.manager.flutter"
- minSdkVersion 26
- targetSdkVersion 33
- versionCode flutterVersionCode.toInteger()
- versionName flutterVersionName
- }
-
- buildTypes {
- release {
- resValue "string", "app_name", "ReVanced Manager"
- signingConfig signingConfigs.debug
- ndk {
- abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
- }
- }
- debug {
- shrinkResources false
- minifyEnabled false
- resValue "string", "app_name", "ReVanced Manager Debug"
- applicationIdSuffix ".debug"
- signingConfig signingConfigs.debug
- ndk {
- abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
- }
- }
- }
-
- packagingOptions {
- exclude '/prebuilt/**'
- }
-}
-
-flutter {
- source '../..'
-}
-
-dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
- // ReVanced
- implementation "app.revanced:revanced-patcher:11.0.4"
-
- // Signing & aligning
- implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
- implementation("com.android.tools.build:apksig:7.2.2")
-
-}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index 2abfc8e943..0000000000
--- a/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 8548324123..0000000000
--- a/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt
deleted file mode 100644
index 2c8d77169e..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt
+++ /dev/null
@@ -1,385 +0,0 @@
-package app.revanced.manager.flutter
-
-import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import androidx.annotation.NonNull
-import app.revanced.manager.flutter.utils.Aapt
-import app.revanced.manager.flutter.utils.aligning.ZipAligner
-import app.revanced.manager.flutter.utils.signing.Signer
-import app.revanced.manager.flutter.utils.zip.ZipFile
-import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
-import app.revanced.patcher.Patcher
-import app.revanced.patcher.PatcherOptions
-import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
-import app.revanced.patcher.extensions.PatchExtensions.patchName
-import app.revanced.patcher.logging.Logger
-import app.revanced.patcher.util.patch.PatchBundle
-import dalvik.system.DexClassLoader
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.MethodChannel
-import java.io.File
-
-private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher"
-private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer"
-
-class MainActivity : FlutterActivity() {
- private val handler = Handler(Looper.getMainLooper())
- private lateinit var installerChannel: MethodChannel
- private var cancel: Boolean = false
- private var stopResult: MethodChannel.Result? = null
-
- override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
- super.configureFlutterEngine(flutterEngine)
- val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL)
- installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
- mainChannel.setMethodCallHandler { call, result ->
- when (call.method) {
- "runPatcher" -> {
- val patchBundleFilePath = call.argument("patchBundleFilePath")
- val originalFilePath = call.argument("originalFilePath")
- val inputFilePath = call.argument("inputFilePath")
- val patchedFilePath = call.argument("patchedFilePath")
- val outFilePath = call.argument("outFilePath")
- val integrationsPath = call.argument("integrationsPath")
- val selectedPatches = call.argument>("selectedPatches")
- val cacheDirPath = call.argument("cacheDirPath")
- val keyStoreFilePath = call.argument("keyStoreFilePath")
- val keystorePassword = call.argument("keystorePassword")
-
- if (patchBundleFilePath != null &&
- originalFilePath != null &&
- inputFilePath != null &&
- patchedFilePath != null &&
- outFilePath != null &&
- integrationsPath != null &&
- selectedPatches != null &&
- cacheDirPath != null &&
- keyStoreFilePath != null &&
- keystorePassword != null
- ) {
- cancel = false
- runPatcher(
- result,
- patchBundleFilePath,
- originalFilePath,
- inputFilePath,
- patchedFilePath,
- outFilePath,
- integrationsPath,
- selectedPatches,
- cacheDirPath,
- keyStoreFilePath,
- keystorePassword
- )
- } else {
- result.notImplemented()
- }
- }
- "stopPatcher" -> {
- cancel = true
- stopResult = result
- }
- else -> result.notImplemented()
- }
- }
- }
-
- private fun runPatcher(
- result: MethodChannel.Result,
- patchBundleFilePath: String,
- originalFilePath: String,
- inputFilePath: String,
- patchedFilePath: String,
- outFilePath: String,
- integrationsPath: String,
- selectedPatches: List,
- cacheDirPath: String,
- keyStoreFilePath: String,
- keystorePassword: String
- ) {
- val originalFile = File(originalFilePath)
- val inputFile = File(inputFilePath)
- val patchedFile = File(patchedFilePath)
- val outFile = File(outFilePath)
- val integrations = File(integrationsPath)
- val keyStoreFile = File(keyStoreFilePath)
-
- Thread {
- try {
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.1,
- "header" to "",
- "log" to "Copying original apk"
- )
- )
- }
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- originalFile.copyTo(inputFile, true)
-
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.2,
- "header" to "Unpacking apk...",
- "log" to "Unpacking input apk"
- )
- )
- }
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- val patcher =
- Patcher(
- PatcherOptions(
- inputFile,
- cacheDirPath,
- Aapt.binary(applicationContext).absolutePath,
- cacheDirPath,
- logger = ManagerLogger()
- )
- )
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf("progress" to 0.3, "header" to "", "log" to "")
- )
- }
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.4,
- "header" to "Merging integrations...",
- "log" to "Merging integrations"
- )
- )
- }
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- patcher.addIntegrations(listOf(integrations)) {}
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.5,
- "header" to "Applying patches...",
- "log" to ""
- )
- )
- }
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
- PatchBundle.Dex(
- patchBundleFilePath,
- DexClassLoader(
- patchBundleFilePath,
- cacheDirPath,
- null,
- javaClass.classLoader
- )
- ).loadPatches().filter { patch ->
- (patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) &&
- selectedPatches.any { it == patch.patchName }
- }
- } else {
- TODO("VERSION.SDK_INT < CUPCAKE")
- }
-
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
-
- patcher.addPatches(patches)
- patcher.executePatches().forEach { (patch, res) ->
- if (res.isSuccess) {
- val msg = "Applied $patch"
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.5,
- "header" to "",
- "log" to msg
- )
- )
- }
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
- return@forEach
- }
- val msg =
- "Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}"
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf("progress" to 0.5, "header" to "", "log" to msg)
- )
- }
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
- }
-
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.7,
- "header" to "Repacking apk...",
- "log" to "Repacking patched apk"
- )
- )
- }
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
- val res = patcher.save()
- ZipFile(patchedFile).use { file ->
- res.dexFiles.forEach {
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
- file.addEntryCompressData(
- ZipEntry.createWithName(it.name),
- it.stream.readBytes()
- )
- }
- res.resourceFile?.let {
- file.copyEntriesFromFileAligned(
- ZipFile(it),
- ZipAligner::getEntryAlignment
- )
- }
- file.copyEntriesFromFileAligned(
- ZipFile(inputFile),
- ZipAligner::getEntryAlignment
- )
- }
- if(cancel) {
- handler.post { stopResult!!.success(null) }
- return@Thread
- }
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 0.9,
- "header" to "Signing apk...",
- "log" to ""
- )
- )
- }
-
- try {
- Signer("ReVanced", keystorePassword).signApk(
- patchedFile,
- outFile,
- keyStoreFile
- )
- } catch (e: Exception) {
- //log to console
- print("Error signing apk: ${e.message}")
- e.printStackTrace()
- }
-
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to 1.0,
- "header" to "Finished!",
- "log" to "Finished!"
- )
- )
- }
- } catch (ex: Throwable) {
- val stack = ex.stackTraceToString()
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf(
- "progress" to -100.0,
- "header" to "Aborted...",
- "log" to "An error occurred! Aborted\nError:\n$stack"
- )
- )
- }
- }
- handler.post { result.success(null) }
- }.start()
- }
-
- inner class ManagerLogger : Logger {
- override fun error(msg: String) {
- handler.post {
- installerChannel
- .invokeMethod(
- "update",
- mapOf("progress" to -1.0, "header" to "", "log" to msg)
- )
- }
- }
-
- override fun warn(msg: String) {
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf("progress" to -1.0, "header" to "", "log" to msg)
- )
- }
- }
-
- override fun info(msg: String) {
- handler.post {
- installerChannel.invokeMethod(
- "update",
- mapOf("progress" to -1.0, "header" to "", "log" to msg)
- )
- }
- }
-
- override fun trace(_msg: String) { /* unused */
- }
- }
-}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt
deleted file mode 100644
index 72198e58fa..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package app.revanced.manager.flutter.utils
-
-import android.content.Context
-import java.io.File
-
-object Aapt {
- fun binary(context: Context): File {
- return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
- }
-}
-
-private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory && f.contains("aapt") }!!.first())
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt
deleted file mode 100644
index 088aad5993..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package app.revanced.manager.flutter.utils.aligning
-
-import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
-
-internal object ZipAligner {
- private const val DEFAULT_ALIGNMENT = 4
- private const val LIBRARY_ALIGNMENT = 4096
-
- fun getEntryAlignment(entry: ZipEntry): Int? =
- if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
-}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt
deleted file mode 100644
index 1e1a08a21c..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package app.revanced.manager.flutter.utils.signing
-
-import com.android.apksig.ApkSigner
-import org.bouncycastle.asn1.x500.X500Name
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
-import org.bouncycastle.cert.X509v3CertificateBuilder
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.bouncycastle.operator.ContentSigner
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.math.BigInteger
-import java.security.*
-import java.security.cert.X509Certificate
-import java.util.*
-
-internal class Signer(
- private val cn: String, password: String
-) {
- private val passwordCharArray = password.toCharArray()
- private fun newKeystore(out: File) {
- val (publicKey, privateKey) = createKey()
- val privateKS = KeyStore.getInstance("BKS", "BC")
- privateKS.load(null, passwordCharArray)
- privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
- privateKS.store(FileOutputStream(out), passwordCharArray)
- }
-
- private fun createKey(): Pair {
- val gen = KeyPairGenerator.getInstance("RSA")
- gen.initialize(2048)
- val pair = gen.generateKeyPair()
- var serialNumber: BigInteger
- do serialNumber =
- BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
- val x500Name = X500Name("CN=$cn")
- val builder = X509v3CertificateBuilder(
- x500Name,
- serialNumber,
- Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
- Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
- Locale.ENGLISH,
- x500Name,
- SubjectPublicKeyInfo.getInstance(pair.public.encoded)
- )
- val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
- return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
- }
-
- fun signApk(input: File, output: File, ks: File) {
- Security.addProvider(BouncyCastleProvider())
-
- if (!ks.exists()) newKeystore(ks)
-
- val keyStore = KeyStore.getInstance("BKS", "BC")
- FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
- val alias = keyStore.aliases().nextElement()
-
- val config = ApkSigner.SignerConfig.Builder(
- cn,
- keyStore.getKey(alias, passwordCharArray) as PrivateKey,
- listOf(keyStore.getCertificate(alias) as X509Certificate)
- ).build()
-
- val signer = ApkSigner.Builder(listOf(config))
- signer.setCreatedBy(cn)
- signer.setInputApk(input)
- signer.setOutputApk(output)
-
- signer.build().sign()
- }
-}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt
deleted file mode 100644
index 3ff0516de5..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-@file:Suppress("unused")
-
-package app.revanced.manager.flutter.utils.zip
-
-import java.io.DataInput
-import java.io.DataOutput
-import java.nio.ByteBuffer
-
-fun UInt.toLittleEndian() =
- (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
-
-fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
-
-fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
- or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt()
-
-fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
-
-fun ByteBuffer.getUShort() = this.short.toUShort()
-fun ByteBuffer.getUInt() = this.int.toUInt()
-
-fun ByteBuffer.putUShort(ushort: UShort): ByteBuffer = this.putShort(ushort.toShort())
-fun ByteBuffer.putUInt(uint: UInt): ByteBuffer = this.putInt(uint.toInt())
-
-fun DataInput.readUShort() = this.readShort().toUShort()
-fun DataInput.readUInt() = this.readInt().toUInt()
-
-fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
-fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
-
-fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
-fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
-
-fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
-fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt
deleted file mode 100644
index 2330938b3b..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt
+++ /dev/null
@@ -1,176 +0,0 @@
-package app.revanced.manager.flutter.utils.zip
-
-import app.revanced.manager.flutter.utils.zip.structures.ZipEndRecord
-import app.revanced.manager.flutter.utils.zip.structures.ZipEntry
-import java.io.Closeable
-import java.io.File
-import java.io.RandomAccessFile
-import java.nio.ByteBuffer
-import java.nio.channels.FileChannel
-import java.util.zip.CRC32
-import java.util.zip.Deflater
-
-class ZipFile(file: File) : Closeable {
- var entries: MutableList = mutableListOf()
-
- private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
- private var CDNeedsRewrite = false
-
- private val compressionLevel = 5
-
- init {
- //if file isn't empty try to load entries
- if (file.length() > 0) {
- val endRecord = findEndRecord()
-
- if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries)
- throw IllegalArgumentException("Multi-file archives are not supported")
-
- entries = readEntries(endRecord).toMutableList()
- }
-
- //seek back to start for writing
- filePointer.seek(0)
- }
-
- private fun findEndRecord(): ZipEndRecord {
- //look from end to start since end record is at the end
- for (i in filePointer.length() - 1 downTo 0) {
- filePointer.seek(i)
- //possible beginning of signature
- if (filePointer.readByte() == 0x50.toByte()) {
- //seek back to get the full int
- filePointer.seek(i)
- val possibleSignature = filePointer.readUIntLE()
- if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
- filePointer.seek(i)
- return ZipEndRecord.fromECD(filePointer)
- }
- }
- }
-
- throw Exception("Couldn't find end record")
- }
-
- private fun readEntries(endRecord: ZipEndRecord): List {
- filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
-
- val numberOfEntries = endRecord.diskEntries.toInt()
-
- return buildList(numberOfEntries) {
- for (i in 1..numberOfEntries) {
- add(
- ZipEntry.fromCDE(filePointer).also
- {
- //for some reason the local extra field can be different from the central one
- it.readLocalExtra(
- filePointer.channel.map(
- FileChannel.MapMode.READ_ONLY,
- it.localHeaderOffset.toLong() + 28,
- 2
- )
- )
- })
- }
- }
- }
-
- private fun writeCD() {
- val CDStart = filePointer.channel.position().toUInt()
-
- entries.forEach {
- filePointer.channel.write(it.toCDE())
- }
-
- val entriesCount = entries.size.toUShort()
-
- val endRecord = ZipEndRecord(
- 0u,
- 0u,
- entriesCount,
- entriesCount,
- filePointer.channel.position().toUInt() - CDStart,
- CDStart,
- ""
- )
-
- filePointer.channel.write(endRecord.toECD())
- }
-
- private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
- CDNeedsRewrite = true
-
- entry.localHeaderOffset = filePointer.channel.position().toUInt()
-
- filePointer.channel.write(entry.toLFH())
- filePointer.channel.write(data)
-
- entries.add(entry)
- }
-
- fun addEntryCompressData(entry: ZipEntry, data: ByteArray) {
- val compressor = Deflater(compressionLevel, true)
- compressor.setInput(data)
- compressor.finish()
-
- val uncompressedSize = data.size
- val compressedData =
- ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
-
- val compressedDataLength = compressor.deflate(compressedData)
- val compressedBuffer =
- ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
-
- compressor.end()
-
- val crc = CRC32()
- crc.update(data)
-
- entry.compression = 8u //deflate compression
- entry.uncompressedSize = uncompressedSize.toUInt()
- entry.compressedSize = compressedDataLength.toUInt()
- entry.crc32 = crc.value.toUInt()
-
- addEntry(entry, compressedBuffer)
- }
-
- private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
- alignment?.let {
- //calculate where data would end up
- val dataOffset = filePointer.filePointer + entry.LFHSize
-
- val mod = dataOffset % alignment
-
- //wrong alignment
- if (mod != 0L) {
- //add padding at end of extra field
- entry.localExtraField =
- entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
- }
- }
-
- addEntry(entry, data)
- }
-
- fun getDataForEntry(entry: ZipEntry): ByteBuffer {
- return filePointer.channel.map(
- FileChannel.MapMode.READ_ONLY,
- entry.dataOffset.toLong(),
- entry.compressedSize.toLong()
- )
- }
-
- fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
- for (entry in file.entries) {
- if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
-
- val data = file.getDataForEntry(entry)
- addEntryCopyData(entry, data, entryAlignment(entry))
- }
- }
-
- override fun close() {
- if (CDNeedsRewrite) writeCD()
- filePointer.close()
- }
-}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt
deleted file mode 100644
index e7b9b58e26..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package app.revanced.manager.flutter.utils.zip.structures
-
-import app.revanced.manager.flutter.utils.zip.putUInt
-import app.revanced.manager.flutter.utils.zip.putUShort
-import app.revanced.manager.flutter.utils.zip.readUIntLE
-import app.revanced.manager.flutter.utils.zip.readUShortLE
-import java.io.DataInput
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-
-data class ZipEndRecord(
- val diskNumber: UShort,
- val startingDiskNumber: UShort,
- val diskEntries: UShort,
- val totalEntries: UShort,
- val centralDirectorySize: UInt,
- val centralDirectoryStartOffset: UInt,
- val fileComment: String,
-) {
-
- companion object {
- const val ECD_HEADER_SIZE = 22
- const val ECD_SIGNATURE = 0x06054b50u
-
- fun fromECD(input: DataInput): ZipEndRecord {
- val signature = input.readUIntLE()
-
- if (signature != ECD_SIGNATURE)
- throw IllegalArgumentException("Input doesn't start with end record signature")
-
- val diskNumber = input.readUShortLE()
- val startingDiskNumber = input.readUShortLE()
- val diskEntries = input.readUShortLE()
- val totalEntries = input.readUShortLE()
- val centralDirectorySize = input.readUIntLE()
- val centralDirectoryStartOffset = input.readUIntLE()
- val fileCommentLength = input.readUShortLE()
- var fileComment = ""
-
- if (fileCommentLength > 0u) {
- val fileCommentBytes = ByteArray(fileCommentLength.toInt())
- input.readFully(fileCommentBytes)
- fileComment = fileCommentBytes.toString(Charsets.UTF_8)
- }
-
- return ZipEndRecord(
- diskNumber,
- startingDiskNumber,
- diskEntries,
- totalEntries,
- centralDirectorySize,
- centralDirectoryStartOffset,
- fileComment
- )
- }
- }
-
- fun toECD(): ByteBuffer {
- val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
-
- val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size)
- .also { it.order(ByteOrder.LITTLE_ENDIAN) }
-
- buffer.putUInt(ECD_SIGNATURE)
- buffer.putUShort(diskNumber)
- buffer.putUShort(startingDiskNumber)
- buffer.putUShort(diskEntries)
- buffer.putUShort(totalEntries)
- buffer.putUInt(centralDirectorySize)
- buffer.putUInt(centralDirectoryStartOffset)
- buffer.putUShort(commentBytes.size.toUShort())
-
- buffer.put(commentBytes)
-
- buffer.flip()
- return buffer
- }
-}
diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt
deleted file mode 100644
index bda1398e7c..0000000000
--- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-package app.revanced.manager.flutter.utils.zip.structures
-
-import app.revanced.manager.flutter.utils.zip.*
-import java.io.DataInput
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-
-data class ZipEntry(
- val version: UShort,
- val versionNeeded: UShort,
- val flags: UShort,
- var compression: UShort,
- val modificationTime: UShort,
- val modificationDate: UShort,
- var crc32: UInt,
- var compressedSize: UInt,
- var uncompressedSize: UInt,
- val diskNumber: UShort,
- val internalAttributes: UShort,
- val externalAttributes: UInt,
- var localHeaderOffset: UInt,
- val fileName: String,
- val extraField: ByteArray,
- val fileComment: String,
- var localExtraField: ByteArray = ByteArray(0), //separate for alignment
-) {
- val LFHSize: Int
- get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
-
- val dataOffset: UInt
- get() = localHeaderOffset + LFHSize.toUInt()
-
- companion object {
- const val CDE_HEADER_SIZE = 46
- const val CDE_SIGNATURE = 0x02014b50u
-
- const val LFH_HEADER_SIZE = 30
- const val LFH_SIGNATURE = 0x04034b50u
-
- fun createWithName(fileName: String): ZipEntry {
- return ZipEntry(
- 0x1403u, //made by unix, version 20
- 0u,
- 0u,
- 0u,
- 0x0821u, //seems to be static time google uses, no idea
- 0x0221u, //same as above
- 0u,
- 0u,
- 0u,
- 0u,
- 0u,
- 0u,
- 0u,
- fileName,
- ByteArray(0),
- ""
- )
- }
-
- fun fromCDE(input: DataInput): ZipEntry {
- val signature = input.readUIntLE()
-
- if (signature != CDE_SIGNATURE)
- throw IllegalArgumentException("Input doesn't start with central directory entry signature")
-
- val version = input.readUShortLE()
- val versionNeeded = input.readUShortLE()
- var flags = input.readUShortLE()
- val compression = input.readUShortLE()
- val modificationTime = input.readUShortLE()
- val modificationDate = input.readUShortLE()
- val crc32 = input.readUIntLE()
- val compressedSize = input.readUIntLE()
- val uncompressedSize = input.readUIntLE()
- val fileNameLength = input.readUShortLE()
- var fileName = ""
- val extraFieldLength = input.readUShortLE()
- val extraField = ByteArray(extraFieldLength.toInt())
- val fileCommentLength = input.readUShortLE()
- var fileComment = ""
- val diskNumber = input.readUShortLE()
- val internalAttributes = input.readUShortLE()
- val externalAttributes = input.readUIntLE()
- val localHeaderOffset = input.readUIntLE()
-
- val variableFieldsLength =
- fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
-
- if (variableFieldsLength > 0) {
- val fileNameBytes = ByteArray(fileNameLength.toInt())
- input.readFully(fileNameBytes)
- fileName = fileNameBytes.toString(Charsets.UTF_8)
-
- input.readFully(extraField)
-
- val fileCommentBytes = ByteArray(fileCommentLength.toInt())
- input.readFully(fileCommentBytes)
- fileComment = fileCommentBytes.toString(Charsets.UTF_8)
- }
-
- flags = (flags and 0b1000u.inv()
- .toUShort()) //disable data descriptor flag as they are not used
-
- return ZipEntry(
- version,
- versionNeeded,
- flags,
- compression,
- modificationTime,
- modificationDate,
- crc32,
- compressedSize,
- uncompressedSize,
- diskNumber,
- internalAttributes,
- externalAttributes,
- localHeaderOffset,
- fileName,
- extraField,
- fileComment,
- )
- }
- }
-
- fun readLocalExtra(buffer: ByteBuffer) {
- buffer.order(ByteOrder.LITTLE_ENDIAN)
- localExtraField = ByteArray(buffer.getUShort().toInt())
- }
-
- fun toLFH(): ByteBuffer {
- val nameBytes = fileName.toByteArray(Charsets.UTF_8)
-
- val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
- .also { it.order(ByteOrder.LITTLE_ENDIAN) }
-
- buffer.putUInt(LFH_SIGNATURE)
- buffer.putUShort(versionNeeded)
- buffer.putUShort(flags)
- buffer.putUShort(compression)
- buffer.putUShort(modificationTime)
- buffer.putUShort(modificationDate)
- buffer.putUInt(crc32)
- buffer.putUInt(compressedSize)
- buffer.putUInt(uncompressedSize)
- buffer.putUShort(nameBytes.size.toUShort())
- buffer.putUShort(localExtraField.size.toUShort())
-
- buffer.put(nameBytes)
- buffer.put(localExtraField)
-
- buffer.flip()
- return buffer
- }
-
- fun toCDE(): ByteBuffer {
- val nameBytes = fileName.toByteArray(Charsets.UTF_8)
- val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
-
- val buffer =
- ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
- .also { it.order(ByteOrder.LITTLE_ENDIAN) }
-
- buffer.putUInt(CDE_SIGNATURE)
- buffer.putUShort(version)
- buffer.putUShort(versionNeeded)
- buffer.putUShort(flags)
- buffer.putUShort(compression)
- buffer.putUShort(modificationTime)
- buffer.putUShort(modificationDate)
- buffer.putUInt(crc32)
- buffer.putUInt(compressedSize)
- buffer.putUInt(uncompressedSize)
- buffer.putUShort(nameBytes.size.toUShort())
- buffer.putUShort(extraField.size.toUShort())
- buffer.putUShort(commentBytes.size.toUShort())
- buffer.putUShort(diskNumber)
- buffer.putUShort(internalAttributes)
- buffer.putUInt(externalAttributes)
- buffer.putUInt(localHeaderOffset)
-
- buffer.put(nameBytes)
- buffer.put(extraField)
- buffer.put(commentBytes)
-
- buffer.flip()
- return buffer
- }
-}
-
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f3f6..0000000000
--- a/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f884..0000000000
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
deleted file mode 100644
index 581c5fca71..0000000000
--- a/android/app/src/main/res/values-night-v31/styles.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 06952be745..0000000000
--- a/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
deleted file mode 100644
index 41f95cf120..0000000000
--- a/android/app/src/main/res/values-v31/styles.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml
deleted file mode 100644
index 74008be87f..0000000000
--- a/android/app/src/main/res/values/ic_launcher_background.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #1B1B1B
-
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index cb1ef88056..0000000000
--- a/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml
deleted file mode 100644
index d2e2c8deaf..0000000000
--- a/android/app/src/main/res/xml/file_paths.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index 2abfc8e943..0000000000
--- a/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
diff --git a/android/build.gradle b/android/build.gradle
deleted file mode 100644
index 88ac491ced..0000000000
--- a/android/build.gradle
+++ /dev/null
@@ -1,37 +0,0 @@
-buildscript {
- ext.kotlin_version = '1.7.10'
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:7.1.3'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- maven {
- url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
- credentials {
- username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String
- password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String
- }
- }
- mavenLocal()
- }
-}
-
-rootProject.buildDir = '../build'
-subprojects {
- project.buildDir = "${rootProject.buildDir}/${project.name}"
- project.evaluationDependsOn(':app')
-}
-
-tasks.register("clean", Delete) {
- delete rootProject.buildDir
-}
diff --git a/android/gradle.properties b/android/gradle.properties
deleted file mode 100644
index 4b11638cfe..0000000000
--- a/android/gradle.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
-org.gradle.parallel=true
-org.gradle.daemon=true
-org.gradle.caching=true
-android.useAndroidX=true
-android.enableJetifier=true
diff --git a/android/settings.gradle b/android/settings.gradle
deleted file mode 100644
index 44e62bcf06..0000000000
--- a/android/settings.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-include ':app'
-
-def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
-def properties = new Properties()
-
-assert localPropertiesFile.exists()
-localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
-
-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"
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000000..211d1ca531
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,211 @@
+import kotlin.random.Random
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.devtools)
+ alias(libs.plugins.about.libraries)
+}
+
+android {
+ namespace = "app.revanced.manager"
+ compileSdk = 35
+ buildToolsVersion = "35.0.1"
+
+ defaultConfig {
+ applicationId = "app.revanced.manager"
+ minSdk = 26
+ targetSdk = 35
+ versionCode = 1
+ versionName = "0.0.1"
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ buildTypes {
+ debug {
+ applicationIdSuffix = ".debug"
+ resValue("string", "app_name", "ReVanced Manager (dev)")
+ isPseudoLocalesEnabled = true
+
+ buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
+ }
+
+ release {
+ if (!project.hasProperty("noProguard")) {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+
+ if (project.hasProperty("signAsDebug")) {
+ applicationIdSuffix = ".debug"
+ resValue("string", "app_name", "ReVanced Manager Debug")
+ signingConfig = signingConfigs.getByName("debug")
+
+ isPseudoLocalesEnabled = true
+ }
+
+ buildConfigField("long", "BUILD_ID", "0L")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ dependenciesInfo {
+ includeInApk = false
+ includeInBundle = false
+ }
+
+ packaging {
+ resources.excludes.addAll(listOf(
+ "/prebuilt/**",
+ "META-INF/DEPENDENCIES",
+ "META-INF/**.version",
+ "DebugProbesKt.bin",
+ "kotlin-tooling-metadata.json",
+ "org/bouncycastle/pqc/**.properties",
+ "org/bouncycastle/x509/**.properties",
+ ))
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas")
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ aidl = true
+ buildConfig = true
+ }
+
+ android {
+ androidResources {
+ generateLocaleConfig = true
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path = file("src/main/cpp/CMakeLists.txt")
+ version = "3.22.1"
+ }
+ }
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+
+dependencies {
+
+ // AndroidX Core
+ implementation(libs.androidx.ktx)
+ implementation(libs.runtime.ktx)
+ implementation(libs.runtime.compose)
+ implementation(libs.splash.screen)
+ implementation(libs.activity.compose)
+ implementation(libs.work.runtime.ktx)
+ implementation(libs.preferences.datastore)
+ implementation(libs.appcompat)
+
+ // Compose
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.ui)
+ implementation(libs.compose.ui.preview)
+ implementation(libs.compose.ui.tooling)
+ implementation(libs.compose.livedata)
+ implementation(libs.compose.material.icons.extended)
+ implementation(libs.compose.material3)
+ implementation(libs.navigation.compose)
+
+ // Accompanist
+ implementation(libs.accompanist.drawablepainter)
+
+ // Placeholder
+ implementation(libs.placeholder.material3)
+
+ // HTML Scraper
+ implementation(libs.skrapeit.dsl)
+ implementation(libs.skrapeit.parser)
+
+ // Coil (async image loading, network image)
+ implementation(libs.coil.compose)
+ implementation(libs.coil.appiconloader)
+
+ // KotlinX
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.collection.immutable)
+ implementation(libs.kotlinx.datetime)
+
+ // Room
+ implementation(libs.room.runtime)
+ implementation(libs.room.ktx)
+ annotationProcessor(libs.room.compiler)
+ ksp(libs.room.compiler)
+
+ // ReVanced
+ implementation(libs.revanced.patcher)
+ implementation(libs.revanced.library)
+
+ // Downloader plugins
+ implementation(project(":downloader-plugin"))
+
+ // Native processes
+ implementation(libs.kotlin.process)
+
+ // HiddenAPI
+ compileOnly(libs.hidden.api.stub)
+
+ // LibSU
+ implementation(libs.libsu.core)
+ implementation(libs.libsu.service)
+ implementation(libs.libsu.nio)
+
+ // Koin
+ implementation(libs.koin.android)
+ implementation(libs.koin.compose)
+ implementation(libs.koin.compose.navigation)
+ implementation(libs.koin.workmanager)
+
+ // Licenses
+ implementation(libs.about.libraries)
+
+ // Ktor
+ implementation(libs.ktor.core)
+ implementation(libs.ktor.logging)
+ implementation(libs.ktor.okhttp)
+ implementation(libs.ktor.content.negotiation)
+ implementation(libs.ktor.serialization)
+
+ // Markdown
+ implementation(libs.markdown.renderer)
+
+ // Fading Edges
+ implementation(libs.fading.edges)
+
+ // Scrollbars
+ implementation(libs.scrollbars)
+
+ // EnumUtil
+ implementation(libs.enumutil)
+ ksp(libs.enumutil.ksp)
+
+ // Reorderable lists
+ implementation(libs.reorderable)
+
+ // Compose Icons
+ implementation(libs.compose.icons.fontawesome)
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000000..b9b9c1aff0
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,63 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+-dontobfuscate
+
+# Required for serialization to work properly
+-if @kotlinx.serialization.Serializable class **
+-keepclassmembers class <1> {
+ static <1>$Companion Companion;
+}
+-if @kotlinx.serialization.Serializable class ** {
+ static **$* *;
+}
+-keepclassmembers class <2>$<3> {
+ kotlinx.serialization.KSerializer serializer(...);
+}
+-if @kotlinx.serialization.Serializable class ** {
+ public static ** INSTANCE;
+}
+-keepclassmembers class <1> {
+ public static <1> INSTANCE;
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# This required for the process runtime.
+-keep class app.revanced.manager.patcher.runtime.process.* {
+ *;
+}
+# Required for the patcher to function correctly
+-keep class app.revanced.patcher.** {
+ *;
+}
+-keep class brut.** {
+ *;
+}
+-keep class org.xmlpull.** {
+ *;
+}
+-keep class kotlin.** {
+ *;
+}
+-keep class org.jf.** {
+ *;
+}
+-keep class com.android.** {
+ *;
+}
+-keep class app.revanced.manager.plugin.** {
+ *;
+}
+
+-dontwarn com.google.auto.value.**
+-dontwarn java.awt.**
+-dontwarn javax.**
+-dontwarn org.slf4j.**
+-dontwarn it.skrape.fetcher.*
+-dontwarn com.google.j2objc.annotations.*
+
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
\ No newline at end of file
diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
new file mode 100644
index 0000000000..fd83a51ed1
--- /dev/null
+++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
@@ -0,0 +1,429 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "d0119047505da435972c5247181de675",
+ "entities": [
+ {
+ "tableName": "patch_bundles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "source",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoUpdate",
+ "columnName": "auto_update",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "patch_selections",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchBundle",
+ "columnName": "patch_bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_patch_selections_patch_bundle_package_name",
+ "unique": true,
+ "columnNames": [
+ "patch_bundle",
+ "package_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "patch_bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "selected_patches",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "selection",
+ "columnName": "selection",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "selection",
+ "patch_name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "patch_selections",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "selection"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "downloaded_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "directory",
+ "columnName": "directory",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsed",
+ "columnName": "last_used",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name",
+ "version"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "installed_app",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
+ "fields": [
+ {
+ "fieldPath": "currentPackageName",
+ "columnName": "current_package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originalPackageName",
+ "columnName": "original_package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "installType",
+ "columnName": "install_type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "current_package_name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "applied_patch",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bundle",
+ "columnName": "bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name",
+ "bundle",
+ "patch_name"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_applied_patch_bundle",
+ "unique": false,
+ "columnNames": [
+ "bundle"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "installed_app",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "package_name"
+ ],
+ "referencedColumns": [
+ "current_package_name"
+ ]
+ },
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "option_groups",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchBundle",
+ "columnName": "patch_bundle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_option_groups_patch_bundle_package_name",
+ "unique": true,
+ "columnNames": [
+ "patch_bundle",
+ "package_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patch_bundles",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "patch_bundle"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "patchName",
+ "columnName": "patch_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "group",
+ "patch_name",
+ "key"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "option_groups",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "group"
+ ],
+ "referencedColumns": [
+ "uid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "trusted_downloader_plugins",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
+ "fields": [
+ {
+ "fieldPath": "packageName",
+ "columnName": "package_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "BLOB",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "package_name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..0404d045d8
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl b/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl
new file mode 100644
index 0000000000..5dbb41c6d8
--- /dev/null
+++ b/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl
@@ -0,0 +1,8 @@
+// IRootService.aidl
+package app.revanced.manager;
+
+// Declare any non-default types here with import statements
+
+interface IRootSystemService {
+ IBinder getFileSystemService();
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl
new file mode 100644
index 0000000000..27a4f61b2a
--- /dev/null
+++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl
@@ -0,0 +1,11 @@
+// IPatcherEvents.aidl
+package app.revanced.manager.patcher.runtime.process;
+
+// Interface for sending events back to the main app process.
+oneway interface IPatcherEvents {
+ void log(String level, String msg);
+ void patchSucceeded();
+ void progress(String name, String state, String msg);
+ // The patching process has ended. The exceptionStackTrace is null if it finished successfully.
+ void finished(String exceptionStackTrace);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl
new file mode 100644
index 0000000000..f938ca6235
--- /dev/null
+++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl
@@ -0,0 +1,14 @@
+// IPatcherProcess.aidl
+package app.revanced.manager.patcher.runtime.process;
+
+import app.revanced.manager.patcher.runtime.process.Parameters;
+import app.revanced.manager.patcher.runtime.process.IPatcherEvents;
+
+interface IPatcherProcess {
+ // Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code.
+ long buildId();
+ // Makes the patcher process exit with code 0
+ oneway void exit();
+ // Starts patching.
+ oneway void start(in Parameters parameters, IPatcherEvents events);
+}
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl
new file mode 100644
index 0000000000..a1e8bee78d
--- /dev/null
+++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl
@@ -0,0 +1,4 @@
+// Parameters.aidl
+package app.revanced.manager.patcher.runtime.process;
+
+parcelable Parameters;
\ No newline at end of file
diff --git a/app/src/main/assets/root/module.prop b/app/src/main/assets/root/module.prop
new file mode 100644
index 0000000000..05a5a159dd
--- /dev/null
+++ b/app/src/main/assets/root/module.prop
@@ -0,0 +1,6 @@
+id=__PKG_NAME__-ReVanced
+name=__LABEL__ ReVanced
+version=__VERSION__
+versionCode=0
+author=ReVanced
+description=Mounts the patched APK on top of the original one
\ No newline at end of file
diff --git a/app/src/main/assets/root/service.sh b/app/src/main/assets/root/service.sh
new file mode 100644
index 0000000000..dc3bcb5f45
--- /dev/null
+++ b/app/src/main/assets/root/service.sh
@@ -0,0 +1,40 @@
+#!/system/bin/sh
+DIR=${0%/*}
+
+package_name="__PKG_NAME__"
+version="__VERSION__"
+
+rm "$DIR/log"
+
+{
+
+until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
+sleep 5
+
+base_path="$DIR/$package_name.apk"
+stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')"
+stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)"
+
+echo "base_path: $base_path"
+echo "stock_path: $stock_path"
+echo "base_version: $version"
+echo "stock_version: $stock_version"
+
+if mount | grep -q "$stock_path" ; then
+ echo "Not mounting as stock path is already mounted"
+ exit 1
+fi
+
+if [ "$version" != "$stock_version" ]; then
+ echo "Not mounting as versions don't match"
+ exit 1
+fi
+
+if [ -z "$stock_path" ]; then
+ echo "Not mounting as app info could not be loaded"
+ exit 1
+fi
+
+mount -o bind "$base_path" "$stock_path"
+
+} >> "$DIR/log"
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000000..64793f8fe5
--- /dev/null
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,38 @@
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html.
+# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
+
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+
+# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
+# Since this is the top level CMakeLists.txt, the project name is also accessible
+# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
+# build script scope).
+project("prop_override")
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+#
+# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
+# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
+# is preferred for the same purpose.
+#
+# In order to load a library into your app from Java/Kotlin, you must call
+# System.loadLibrary() and pass the name of the library defined here;
+# for GameActivity/NativeActivity derived applications, the same library name must be
+# used in the AndroidManifest.xml file.
+add_library(${CMAKE_PROJECT_NAME} SHARED
+ # List C/C++ source files with relative paths to this CMakeLists.txt.
+ prop_override.cpp)
+
+# Specifies libraries CMake should link to your target library. You
+# can link libraries from various origins, such as libraries defined in this
+# build script, prebuilt third-party libraries, or Android system libraries.
+target_link_libraries(${CMAKE_PROJECT_NAME}
+ # List libraries link to the target library
+ android
+ log)
diff --git a/app/src/main/cpp/prop_override.cpp b/app/src/main/cpp/prop_override.cpp
new file mode 100644
index 0000000000..b314ccd117
--- /dev/null
+++ b/app/src/main/cpp/prop_override.cpp
@@ -0,0 +1,62 @@
+// Library for overriding Android system properties via environment variables.
+//
+// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize
+// Output: 123M
+#include
+#include
+#include
+#include
+
+// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h
+#define PROP_VALUE_MAX 92
+// This is the mangled name of "android::base::GetProperty".
+#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"
+
+extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *);
+typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &);
+
+char *GetPropOverride(const std::string &key) {
+ auto envKey = "PROP_" + key;
+
+ return getenv(envKey.c_str());
+}
+
+// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp
+extern "C" int property_get(const char *key, char *value, const char *default_value) {
+ auto replacement = GetPropOverride(std::string(key));
+ if (replacement) {
+ int len = strnlen(replacement, PROP_VALUE_MAX);
+
+ strncpy(value, replacement, len);
+ return len;
+ }
+
+ static property_get_ptr original = NULL;
+ if (!original) {
+ // Get the address of the original function.
+ original = reinterpret_cast(dlsym(RTLD_NEXT, "property_get"));
+ }
+
+ return original(key, value, default_value);
+}
+
+// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library.
+// We can get around this by forcing the function to adopt a specific name using the asm keyword.
+std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME);
+
+
+// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp
+// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future.
+std::string GetProperty(const std::string &key, const std::string &default_value) {
+ auto replacement = GetPropOverride(key);
+ if (replacement) {
+ return std::string(replacement);
+ }
+
+ static GetProperty_ptr original = NULL;
+ if (!original) {
+ original = reinterpret_cast(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME));
+ }
+
+ return original(key, default_value);
+}
diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt
new file mode 100644
index 0000000000..60399f10f1
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/MainActivity.kt
@@ -0,0 +1,335 @@
+package app.revanced.manager
+
+import android.content.ActivityNotFoundException
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
+import app.revanced.manager.ui.model.navigation.AppSelector
+import app.revanced.manager.ui.model.navigation.ComplexParameter
+import app.revanced.manager.ui.model.navigation.Dashboard
+import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
+import app.revanced.manager.ui.model.navigation.Patcher
+import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
+import app.revanced.manager.ui.model.navigation.Settings
+import app.revanced.manager.ui.model.navigation.Update
+import app.revanced.manager.ui.screen.AppSelectorScreen
+import app.revanced.manager.ui.screen.DashboardScreen
+import app.revanced.manager.ui.screen.InstalledAppInfoScreen
+import app.revanced.manager.ui.screen.PatcherScreen
+import app.revanced.manager.ui.screen.PatchesSelectorScreen
+import app.revanced.manager.ui.screen.RequiredOptionsScreen
+import app.revanced.manager.ui.screen.SelectedAppInfoScreen
+import app.revanced.manager.ui.screen.SettingsScreen
+import app.revanced.manager.ui.screen.UpdateScreen
+import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
+import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
+import app.revanced.manager.ui.screen.settings.ContributorScreen
+import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen
+import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
+import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
+import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
+import app.revanced.manager.ui.screen.settings.LicensesScreen
+import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
+import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
+import app.revanced.manager.ui.theme.ReVancedManagerTheme
+import app.revanced.manager.ui.theme.Theme
+import app.revanced.manager.ui.viewmodel.MainViewModel
+import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
+import app.revanced.manager.util.EventEffect
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+import org.koin.androidx.compose.navigation.koinNavViewModel
+import org.koin.core.parameter.parametersOf
+import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
+
+class MainActivity : ComponentActivity() {
+ @ExperimentalAnimationApi
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ enableEdgeToEdge()
+ installSplashScreen()
+
+ val vm: MainViewModel = getActivityViewModel()
+
+ setContent {
+ val launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ onResult = vm::applyLegacySettings
+ )
+ val theme by vm.prefs.theme.getAsState()
+ val dynamicColor by vm.prefs.dynamicColor.getAsState()
+
+ EventEffect(vm.legacyImportActivityFlow) {
+ try {
+ launcher.launch(it)
+ } catch (_: ActivityNotFoundException) {
+ }
+ }
+
+ ReVancedManagerTheme(
+ darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
+ dynamicColor = dynamicColor
+ ) {
+ ReVancedManager(vm)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ReVancedManager(vm: MainViewModel) {
+ val navController = rememberNavController()
+
+ EventEffect(vm.appSelectFlow) { app ->
+ navController.navigateComplex(
+ SelectedApplicationInfo,
+ SelectedApplicationInfo.ViewModelParams(app)
+ )
+ }
+
+ NavHost(
+ navController = navController,
+ startDestination = Dashboard,
+ enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
+ exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
+ popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
+ popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
+ ) {
+ composable {
+ DashboardScreen(
+ onSettingsClick = { navController.navigate(Settings) },
+ onAppSelectorClick = {
+ navController.navigate(AppSelector)
+ },
+ onUpdateClick = {
+ navController.navigate(Update())
+ },
+ onDownloaderPluginClick = {
+ navController.navigate(Settings.Downloads)
+ },
+ onAppClick = { packageName ->
+ navController.navigate(InstalledApplicationInfo(packageName))
+ }
+ )
+ }
+
+ composable {
+ val data = it.toRoute()
+
+ InstalledAppInfoScreen(
+ onPatchClick = vm::selectApp,
+ onBackClick = navController::popBackStack,
+ viewModel = koinViewModel { parametersOf(data.packageName) }
+ )
+ }
+
+ composable {
+ AppSelectorScreen(
+ onSelect = vm::selectApp,
+ onStorageSelect = vm::selectApp,
+ onBackClick = navController::popBackStack
+ )
+ }
+
+ composable {
+ PatcherScreen(
+ onBackClick = {
+ navController.navigate(route = Dashboard) {
+ launchSingleTop = true
+ popUpTo {
+ inclusive = false
+ }
+ }
+ },
+ vm = koinViewModel { parametersOf(it.getComplexArg()) }
+ )
+ }
+
+ composable {
+ val data = it.toRoute()
+
+ UpdateScreen(
+ onBackClick = navController::popBackStack,
+ vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
+ )
+ }
+
+ navigation(startDestination = SelectedApplicationInfo.Main) {
+ composable {
+ val parentBackStackEntry = navController.navGraphEntry(it)
+ val data =
+ parentBackStackEntry.getComplexArg()
+ val viewModel =
+ koinNavViewModel(viewModelStoreOwner = parentBackStackEntry) {
+ parametersOf(data)
+ }
+
+ SelectedAppInfoScreen(
+ onBackClick = navController::popBackStack,
+ onPatchClick = {
+ it.lifecycleScope.launch {
+ navController.navigateComplex(
+ Patcher,
+ viewModel.getPatcherParams()
+ )
+ }
+ },
+ onPatchSelectorClick = { app, patches, options ->
+ navController.navigateComplex(
+ SelectedApplicationInfo.PatchesSelector,
+ SelectedApplicationInfo.PatchesSelector.ViewModelParams(
+ app,
+ patches,
+ options
+ )
+ )
+ },
+ onRequiredOptions = { app, patches, options ->
+ navController.navigateComplex(
+ SelectedApplicationInfo.RequiredOptions,
+ SelectedApplicationInfo.PatchesSelector.ViewModelParams(
+ app,
+ patches,
+ options
+ )
+ )
+ },
+ vm = viewModel
+ )
+ }
+
+ composable {
+ val data =
+ it.getComplexArg()
+ val selectedAppInfoVm = koinNavViewModel(
+ viewModelStoreOwner = navController.navGraphEntry(it)
+ )
+
+ PatchesSelectorScreen(
+ onBackClick = navController::popBackStack,
+ onSave = { patches, options ->
+ selectedAppInfoVm.updateConfiguration(patches, options)
+ navController.popBackStack()
+ },
+ vm = koinViewModel { parametersOf(data) }
+ )
+ }
+
+ composable {
+ val data =
+ it.getComplexArg()
+ val selectedAppInfoVm = koinNavViewModel(
+ viewModelStoreOwner = navController.navGraphEntry(it)
+ )
+
+ RequiredOptionsScreen(
+ onBackClick = navController::popBackStack,
+ onContinue = { patches, options ->
+ selectedAppInfoVm.updateConfiguration(patches, options)
+ it.lifecycleScope.launch {
+ navController.navigateComplex(
+ Patcher,
+ selectedAppInfoVm.getPatcherParams()
+ )
+ }
+ },
+ vm = koinViewModel { parametersOf(data) }
+ )
+ }
+ }
+
+ navigation(startDestination = Settings.Main) {
+ composable {
+ SettingsScreen(
+ onBackClick = navController::popBackStack,
+ navigate = navController::navigate
+ )
+ }
+
+ composable {
+ GeneralSettingsScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ AdvancedSettingsScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ UpdatesSettingsScreen(
+ onBackClick = navController::popBackStack,
+ onChangelogClick = { navController.navigate(Settings.Changelogs) },
+ onUpdateClick = { navController.navigate(Update()) }
+ )
+ }
+
+ composable {
+ DownloadsSettingsScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ ImportExportSettingsScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ AboutSettingsScreen(
+ onBackClick = navController::popBackStack,
+ navigate = navController::navigate
+ )
+ }
+
+ composable {
+ ChangelogsScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ ContributorScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ LicensesScreen(onBackClick = navController::popBackStack)
+ }
+
+ composable {
+ DeveloperOptionsScreen(onBackClick = navController::popBackStack)
+ }
+ }
+ }
+}
+
+@Composable
+private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
+ remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
+
+// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
+private fun > NavController.navigateComplex(
+ route: R,
+ data: T
+) {
+ navigate(route)
+ getBackStackEntry(route).savedStateHandle["args"] = data
+}
+
+private fun NavBackStackEntry.getComplexArg() = savedStateHandle.get("args")!!
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
new file mode 100644
index 0000000000..1d17e5ef61
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
@@ -0,0 +1,110 @@
+package app.revanced.manager
+
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+import android.util.Log
+import app.revanced.manager.data.platform.Filesystem
+import app.revanced.manager.di.*
+import app.revanced.manager.domain.manager.PreferencesManager
+import app.revanced.manager.domain.repository.DownloaderPluginRepository
+import app.revanced.manager.domain.repository.PatchBundleRepository
+import app.revanced.manager.util.tag
+import kotlinx.coroutines.Dispatchers
+import coil.Coil
+import coil.ImageLoader
+import com.topjohnwu.superuser.Shell
+import com.topjohnwu.superuser.internal.BuilderImpl
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import me.zhanghai.android.appiconloader.coil.AppIconFetcher
+import me.zhanghai.android.appiconloader.coil.AppIconKeyer
+import org.koin.android.ext.android.inject
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.androidx.workmanager.koin.workManagerFactory
+import org.koin.core.context.startKoin
+
+class ManagerApplication : Application() {
+ private val scope = MainScope()
+ private val prefs: PreferencesManager by inject()
+ private val patchBundleRepository: PatchBundleRepository by inject()
+ private val downloaderPluginRepository: DownloaderPluginRepository by inject()
+ private val fs: Filesystem by inject()
+
+ override fun onCreate() {
+ super.onCreate()
+
+ startKoin {
+ androidContext(this@ManagerApplication)
+ androidLogger()
+ workManagerFactory()
+ modules(
+ httpModule,
+ preferencesModule,
+ repositoryModule,
+ serviceModule,
+ managerModule,
+ workerModule,
+ viewModelModule,
+ databaseModule,
+ rootModule
+ )
+ }
+
+ val pixels = 512
+ Coil.setImageLoader(
+ ImageLoader.Builder(this)
+ .components {
+ add(AppIconKeyer())
+ add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
+ }
+ .build()
+ )
+
+ val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
+ Shell.setDefaultBuilder(shellBuilder)
+
+ scope.launch {
+ prefs.preload()
+ }
+ scope.launch(Dispatchers.Default) {
+ downloaderPluginRepository.reload()
+ }
+ scope.launch(Dispatchers.Default) {
+ with(patchBundleRepository) {
+ reload()
+ updateCheck()
+ }
+ }
+ registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
+ private var firstActivityCreated = false
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ if (firstActivityCreated) return
+ firstActivityCreated = true
+
+ // We do not want to call onFreshProcessStart() if there is state to restore.
+ // This can happen on system-initiated process death.
+ if (savedInstanceState == null) {
+ Log.d(tag, "Fresh process created")
+ onFreshProcessStart()
+ } else Log.d(tag, "System-initiated process death detected")
+ }
+
+ override fun onActivityStarted(activity: Activity) {}
+ override fun onActivityResumed(activity: Activity) {}
+ override fun onActivityPaused(activity: Activity) {}
+ override fun onActivityStopped(activity: Activity) {}
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+ override fun onActivityDestroyed(activity: Activity) {}
+ })
+ }
+
+ private fun onFreshProcessStart() {
+ fs.uiTempDir.apply {
+ deleteRecursively()
+ mkdirs()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt
new file mode 100644
index 0000000000..7bad2debc3
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt
@@ -0,0 +1,51 @@
+package app.revanced.manager.data.platform
+
+import android.Manifest
+import android.app.Application
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Environment
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+import app.revanced.manager.util.RequestManageStorageContract
+import java.io.File
+import java.nio.file.Path
+
+class Filesystem(private val app: Application) {
+ val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
+
+ /**
+ * A directory that gets cleared when the app restarts.
+ * Do not store paths to this directory in a parcel.
+ */
+ val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
+ deleteRecursively()
+ mkdirs()
+ }
+
+ /**
+ * A directory for storing temporary files related to UI.
+ * This is the same as [tempDir], but does not get cleared on system-initiated process death.
+ * Paths to this directory can be safely stored in parcels.
+ */
+ val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
+
+ fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
+
+ private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+
+ private val storagePermissionName =
+ if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
+
+ fun permissionContract(): Pair, String> {
+ val contract =
+ if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
+ return contract to storagePermissionName
+ }
+
+ fun hasStoragePermission() =
+ if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
+ storagePermissionName
+ ) == PackageManager.PERMISSION_GRANTED
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt
new file mode 100644
index 0000000000..f5d3dd89bb
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt
@@ -0,0 +1,19 @@
+package app.revanced.manager.data.platform
+
+import android.app.Application
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import androidx.core.content.getSystemService
+
+class NetworkInfo(app: Application) {
+ private val connectivityManager = app.getSystemService()!!
+
+ private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
+ fun isConnected() = connectivityManager.activeNetwork != null
+ fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true
+
+ /**
+ * Returns true if it is safe to download large files.
+ */
+ fun isSafe() = isConnected() && isUnmetered()
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt
new file mode 100644
index 0000000000..403bd1cf71
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt
@@ -0,0 +1,39 @@
+package app.revanced.manager.data.room
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
+import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
+import app.revanced.manager.data.room.apps.installed.AppliedPatch
+import app.revanced.manager.data.room.apps.installed.InstalledApp
+import app.revanced.manager.data.room.apps.installed.InstalledAppDao
+import app.revanced.manager.data.room.selection.PatchSelection
+import app.revanced.manager.data.room.selection.SelectedPatch
+import app.revanced.manager.data.room.selection.SelectionDao
+import app.revanced.manager.data.room.bundles.PatchBundleDao
+import app.revanced.manager.data.room.bundles.PatchBundleEntity
+import app.revanced.manager.data.room.options.Option
+import app.revanced.manager.data.room.options.OptionDao
+import app.revanced.manager.data.room.options.OptionGroup
+import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
+import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
+import kotlin.random.Random
+
+@Database(
+ entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
+ version = 1
+)
+@TypeConverters(Converters::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun patchBundleDao(): PatchBundleDao
+ abstract fun selectionDao(): SelectionDao
+ abstract fun downloadedAppDao(): DownloadedAppDao
+ abstract fun installedAppDao(): InstalledAppDao
+ abstract fun optionDao(): OptionDao
+ abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
+
+ companion object {
+ fun generateUid() = Random.Default.nextInt()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/Converters.kt b/app/src/main/java/app/revanced/manager/data/room/Converters.kt
new file mode 100644
index 0000000000..a9437f86e2
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/Converters.kt
@@ -0,0 +1,26 @@
+package app.revanced.manager.data.room
+
+import androidx.room.TypeConverter
+import app.revanced.manager.data.room.bundles.Source
+import app.revanced.manager.data.room.options.Option.SerializedValue
+import java.io.File
+
+class Converters {
+ @TypeConverter
+ fun sourceFromString(value: String) = Source.from(value)
+
+ @TypeConverter
+ fun sourceToString(value: Source) = value.toString()
+
+ @TypeConverter
+ fun fileFromString(value: String) = File(value)
+
+ @TypeConverter
+ fun fileToString(file: File): String = file.path
+
+ @TypeConverter
+ fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value)
+
+ @TypeConverter
+ fun serializedOptionToString(value: SerializedValue) = value.toJsonString()
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
new file mode 100644
index 0000000000..f170331448
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt
@@ -0,0 +1,16 @@
+package app.revanced.manager.data.room.apps.downloaded
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import java.io.File
+
+@Entity(
+ tableName = "downloaded_app",
+ primaryKeys = ["package_name", "version"]
+)
+data class DownloadedApp(
+ @ColumnInfo(name = "package_name") val packageName: String,
+ @ColumnInfo(name = "version") val version: String,
+ @ColumnInfo(name = "directory") val directory: File,
+ @ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt
new file mode 100644
index 0000000000..492dbde16c
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt
@@ -0,0 +1,26 @@
+package app.revanced.manager.data.room.apps.downloaded
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Upsert
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface DownloadedAppDao {
+ @Query("SELECT * FROM downloaded_app")
+ fun getAllApps(): Flow>
+
+ @Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
+ suspend fun get(packageName: String, version: String): DownloadedApp?
+
+ @Upsert
+ suspend fun upsert(downloadedApp: DownloadedApp)
+
+ @Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
+ suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
+
+ @Delete
+ suspend fun delete(downloadedApps: Collection)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt
new file mode 100644
index 0000000000..d2a498a3a0
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt
@@ -0,0 +1,35 @@
+package app.revanced.manager.data.room.apps.installed
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import app.revanced.manager.data.room.bundles.PatchBundleEntity
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@Entity(
+ tableName = "applied_patch",
+ primaryKeys = ["package_name", "bundle", "patch_name"],
+ foreignKeys = [
+ ForeignKey(
+ InstalledApp::class,
+ parentColumns = ["current_package_name"],
+ childColumns = ["package_name"],
+ onDelete = ForeignKey.CASCADE
+ ),
+ ForeignKey(
+ PatchBundleEntity::class,
+ parentColumns = ["uid"],
+ childColumns = ["bundle"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [Index(value = ["bundle"], unique = false)]
+)
+data class AppliedPatch(
+ @ColumnInfo(name = "package_name") val packageName: String,
+ @ColumnInfo(name = "bundle") val bundle: Int,
+ @ColumnInfo(name = "patch_name") val patchName: String
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt
new file mode 100644
index 0000000000..c0986dfd10
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt
@@ -0,0 +1,20 @@
+package app.revanced.manager.data.room.apps.installed
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import app.revanced.manager.R
+
+enum class InstallType(val stringResource: Int) {
+ DEFAULT(R.string.default_install),
+ MOUNT(R.string.mount_install)
+}
+
+@Entity(tableName = "installed_app")
+data class InstalledApp(
+ @PrimaryKey
+ @ColumnInfo(name = "current_package_name") val currentPackageName: String,
+ @ColumnInfo(name = "original_package_name") val originalPackageName: String,
+ @ColumnInfo(name = "version") val version: String,
+ @ColumnInfo(name = "install_type") val installType: InstallType
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt
new file mode 100644
index 0000000000..c290cc5e9e
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt
@@ -0,0 +1,46 @@
+package app.revanced.manager.data.room.apps.installed
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.MapColumn
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Upsert
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface InstalledAppDao {
+ @Query("SELECT * FROM installed_app")
+ fun getAll(): Flow>
+
+ @Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
+ suspend fun get(packageName: String): InstalledApp?
+
+ @Query(
+ "SELECT bundle, patch_name FROM applied_patch" +
+ " WHERE package_name = :packageName"
+ )
+ suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn(
+ "patch_name"
+ ) String>>
+
+ @Transaction
+ suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List) {
+ upsertApp(installedApp)
+ deleteAppliedPatches(installedApp.currentPackageName)
+ insertAppliedPatches(appliedPatches)
+ }
+
+ @Upsert
+ suspend fun upsertApp(installedApp: InstalledApp)
+
+ @Insert
+ suspend fun insertAppliedPatches(appliedPatches: List)
+
+ @Query("DELETE FROM applied_patch WHERE package_name = :packageName")
+ suspend fun deleteAppliedPatches(packageName: String)
+
+ @Delete
+ suspend fun delete(installedApp: InstalledApp)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
new file mode 100644
index 0000000000..d9955a702b
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt
@@ -0,0 +1,37 @@
+package app.revanced.manager.data.room.bundles
+
+import androidx.room.*
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface PatchBundleDao {
+ @Query("SELECT * FROM patch_bundles")
+ suspend fun all(): List
+
+ @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
+ fun getPropsById(uid: Int): Flow
+
+ @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
+ suspend fun updateVersion(uid: Int, patches: String?)
+
+ @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
+ suspend fun setAutoUpdate(uid: Int, value: Boolean)
+
+ @Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
+ suspend fun setName(uid: Int, value: String)
+
+ @Query("DELETE FROM patch_bundles WHERE uid != 0")
+ suspend fun purgeCustomBundles()
+
+ @Transaction
+ suspend fun reset() {
+ purgeCustomBundles()
+ updateVersion(0, null) // Reset the main source
+ }
+
+ @Query("DELETE FROM patch_bundles WHERE uid = :uid")
+ suspend fun remove(uid: Int)
+
+ @Insert
+ suspend fun add(source: PatchBundleEntity)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
new file mode 100644
index 0000000000..8ba5f64a96
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt
@@ -0,0 +1,44 @@
+package app.revanced.manager.data.room.bundles
+
+import androidx.room.*
+import io.ktor.http.*
+
+sealed class Source {
+ object Local : Source() {
+ const val SENTINEL = "local"
+
+ override fun toString() = SENTINEL
+ }
+
+ object API : Source() {
+ const val SENTINEL = "api"
+
+ override fun toString() = SENTINEL
+ }
+
+ data class Remote(val url: Url) : Source() {
+ override fun toString() = url.toString()
+ }
+
+ companion object {
+ fun from(value: String) = when (value) {
+ Local.SENTINEL -> Local
+ API.SENTINEL -> API
+ else -> Remote(Url(value))
+ }
+ }
+}
+
+@Entity(tableName = "patch_bundles")
+data class PatchBundleEntity(
+ @PrimaryKey val uid: Int,
+ @ColumnInfo(name = "name") val name: String,
+ @ColumnInfo(name = "version") val version: String? = null,
+ @ColumnInfo(name = "source") val source: Source,
+ @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
+)
+
+data class BundleProperties(
+ @ColumnInfo(name = "version") val version: String? = null,
+ @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
+)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt
new file mode 100644
index 0000000000..44bc3d40ab
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt
@@ -0,0 +1,116 @@
+package app.revanced.manager.data.room.options
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import app.revanced.manager.patcher.patch.Option
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.add
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.float
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+
+@Entity(
+ tableName = "options",
+ primaryKeys = ["group", "patch_name", "key"],
+ foreignKeys = [ForeignKey(
+ OptionGroup::class,
+ parentColumns = ["uid"],
+ childColumns = ["group"],
+ onDelete = ForeignKey.CASCADE
+ )]
+)
+data class Option(
+ @ColumnInfo(name = "group") val group: Int,
+ @ColumnInfo(name = "patch_name") val patchName: String,
+ @ColumnInfo(name = "key") val key: String,
+ // Encoded as Json.
+ @ColumnInfo(name = "value") val value: SerializedValue,
+) {
+ @Serializable
+ data class SerializedValue(val raw: JsonElement) {
+ fun toJsonString() = json.encodeToString(raw)
+ fun deserializeFor(option: Option<*>): Any? {
+ if (raw is JsonNull) return null
+
+ val errorMessage = "Cannot deserialize value as ${option.type}"
+ try {
+ if (option.type.classifier == List::class) {
+ val elementType = option.type.arguments.first().type!!
+ return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
+ }
+
+ return deserializeBasicType(option.type, raw.jsonPrimitive)
+ } catch (e: IllegalArgumentException) {
+ throw SerializationException(errorMessage, e)
+ } catch (e: IllegalStateException) {
+ throw SerializationException(errorMessage, e)
+ } catch (e: kotlinx.serialization.SerializationException) {
+ throw SerializationException(errorMessage, e)
+ }
+ }
+
+ companion object {
+ private val json = Json {
+ // Patcher does not forbid the use of these values, so we should support them.
+ allowSpecialFloatingPointValues = true
+ }
+
+ private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
+ typeOf() -> value.boolean
+ typeOf() -> value.int
+ typeOf() -> value.long
+ typeOf() -> value.float
+ typeOf() -> value.content.also {
+ if (!value.isString) throw SerializationException(
+ "Expected value to be a string: $value"
+ )
+ }
+
+ else -> throw SerializationException("Unknown type: $type")
+ }
+
+ fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value))
+ fun fromValue(value: Any?) = SerializedValue(when (value) {
+ null -> JsonNull
+ is Number -> JsonPrimitive(value)
+ is Boolean -> JsonPrimitive(value)
+ is String -> JsonPrimitive(value)
+ is List<*> -> buildJsonArray {
+ var elementClass: KClass? = null
+
+ value.forEach {
+ when (it) {
+ null -> throw SerializationException("List elements must not be null")
+ is Number -> add(it)
+ is Boolean -> add(it)
+ is String -> add(it)
+ else -> throw SerializationException("Unknown element type: ${it::class.simpleName}")
+ }
+
+ if (elementClass == null) elementClass = it::class
+ else if (elementClass != it::class) throw SerializationException("List elements must have the same type")
+ }
+ }
+
+ else -> throw SerializationException("Unknown type: ${value::class.simpleName}")
+ })
+ }
+ }
+
+ class SerializationException(message: String, cause: Throwable? = null) :
+ Exception(message, cause)
+}
diff --git a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt
new file mode 100644
index 0000000000..5a147f6f3b
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt
@@ -0,0 +1,50 @@
+package app.revanced.manager.data.room.options
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.MapColumn
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+abstract class OptionDao {
+ @Transaction
+ @Query(
+ "SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
+ " LEFT JOIN options ON uid = options.`group`" +
+ " WHERE package_name = :packageName"
+ )
+ abstract suspend fun getOptions(packageName: String): Map<@MapColumn("patch_bundle") Int, List