diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7808c795d1..883bf52a7a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: 🐞 Bug report description: Report a bug or an issue. -title: "bug: " -labels: ["Bug report"] +title: 'bug: ' +labels: ['Bug report'] body: - type: markdown attributes: @@ -80,38 +80,21 @@ body: - Describe your bug in detail - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) - Add images and videos if possible - - List used patches if applicable + - List used patches, downloader and settings if applicable validations: required: true - type: textarea attributes: - label: Version of ReVanced Manager and version & name of app you are patching - validations: - required: true - - type: dropdown - attributes: - label: Installation method - options: - - Regular - - Mount - validations: - required: false - - type: textarea - attributes: - label: ReVanced Manager logs - description: Export logs from the ReVanced Manager settings. + label: Patch logs + description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes. render: shell - validations: - required: true - type: textarea attributes: - label: Patch logs - description: Export logs from the "Patcher" screen. - render: shell + label: Debug logs + description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced". validations: - required: false + required: true - type: checkboxes - id: acknowledgements attributes: label: Acknowledgements description: Your bug report will be closed if you don't follow the checklist below. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ee6d407c9..6868beb0fb 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: false contact_links: - name: 🗨 Discussions url: https://github.com/revanced/revanced-suggestions/discussions - about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion! + about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f52fe568c8..8d3366b3e6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,7 +1,3 @@ -name: ⭐ Feature request -description: Create a detailed request for a new feature. -title: "feat: " -labels: ["Feature request"] body: - type: markdown attributes: @@ -84,7 +80,7 @@ body: label: Motivation description: | A strong motivation is necessary for a feature request to be considered. - + - Why should this feature be implemented? - What is the explicit use case? - What are the benefits? @@ -97,9 +93,11 @@ body: label: Acknowledgements description: Your feature request will be closed if you don't follow the checklist below. options: - - label: I have checked all open and closed feature requests and this is not a duplicate. + - label: I have checked all open and closed feature requests and this is not a duplicate required: true - label: I have chosen an appropriate title. required: true + - label: All requested information has been provided properly. + required: true - label: The feature request is only related to ReVanced Manager. required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 44909febb3..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,78 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - labels: [] - directory: / - target-branch: dev - schedule: - interval: monthly - groups: - gh-actions: - applies-to: version-updates - patterns: - - "*" - update-types: - - "minor" - - "patch" - - - package-ecosystem: npm - labels: [] - directory: / - target-branch: dev - schedule: - interval: monthly - groups: - npm: - applies-to: version-updates - patterns: - - "*" - update-types: - - "minor" - - "patch" - - # ReVanced Manager Flutter - - package-ecosystem: pub - labels: [] - directory: / - target-branch: dev - schedule: - interval: monthly - groups: - pubspec: - applies-to: version-updates - patterns: - - "*" - update-types: - - "minor" - - "patch" - - - package-ecosystem: gradle - labels: [] - directory: /android - target-branch: dev - schedule: - interval: monthly - groups: - gradle: - applies-to: version-updates - patterns: - - "*" - update-types: - - "minor" - - "patch" - - # ReVanced Manager Compose - - package-ecosystem: gradle - labels: [ "ReVanced Manager Compose" ] - directory: / - target-branch: compose-dev - schedule: - interval: monthly - groups: - gradle-compose: - applies-to: version-updates - patterns: - - "*" - update-types: - - "minor" - - "patch" diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 1b4400b325..81b1a13d4c 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -2,69 +2,37 @@ name: Build pull request on: workflow_dispatch: - inputs: - pr-number: - description: PR number - required: true - app-flavor: - description: App flavor - default: release - type: choice - options: - - release - - debug - - profile + pull_request: + branches: + - dev + - compose-dev jobs: - build: + release: name: Build runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write steps: - - name: Checkout PR - uses: actions/checkout@v4 - with: - ref: refs/pull/${{ inputs.pr-number }}/merge - fetch-depth: 0 + - name: Checkout + uses: actions/checkout@v5 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - name: Cache Gradle - uses: burrunan/gradle-cache-action@v1 - with: - build-root-directory: ${{ github.workspace }}/android - - - name: Get dependencies - run: flutter pub get - - - name: Generate translations - run: dart run slang - - - name: Generate code files - run: dart run build_runner build --delete-conflicting-outputs + uses: burrunan/gradle-cache-action@v3 - name: Build - id: flutter-build - run: flutter build apk --${{ inputs.app-flavor }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew assembleRelease --no-daemon - name: Upload artifacts - if: steps.flutter-build.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: revanced-manager-(${{ env.COMMIT_HASH }} + name: revanced-manager path: | - build/app/outputs/flutter-apk/app-*.apk + app/build/outputs/apk/release/revanced-manager*.apk + app/build/outputs/apk/release/revanced-manager*.apk.asc diff --git a/.github/workflows/open_pull_request.yml b/.github/workflows/open_pull_request.yml index 03d40f7f55..b86deb7327 100644 --- a/.github/workflows/open_pull_request.yml +++ b/.github/workflows/open_pull_request.yml @@ -12,18 +12,15 @@ env: jobs: pull-request: name: Open pull request - permissions: - pull-requests: write runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Open pull request uses: repo-sync/pull-request@v2 with: destination_branch: 'main' pr_title: 'chore: ${{ env.MESSAGE }}' - pr_body: | - This pull request will ${{ env.MESSAGE }}. + pr_body: 'This pull request will ${{ env.MESSAGE }}.' pr_draft: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c3a2b4580..49f5872549 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,60 +17,54 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 + uses: actions/checkout@v5 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' + - name: Cache Gradle + uses: burrunan/gradle-cache-action@v3 + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew assembleRelease + - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: "lts/*" cache: 'npm' - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - cache: true - - - name: Cache Gradle - uses: burrunan/gradle-cache-action@v1 - with: - build-root-directory: ${{ github.workspace }}/android - - name: Install dependencies - run: npm i - - - name: Get dependencies - run: flutter pub get + run: npm ci - - name: Generate translations - run: dart run slang - - - name: Generate code files - run: dart run build_runner build --delete-conflicting-outputs + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + fingerprint: ${{ vars.GPG_FINGERPRINT }} - name: Setup keystore run: | - echo "${{ secrets.KEYSTORE }}" | base64 --decode > "android/app/keystore.jks" + echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks" - name: Semantic Release - uses: cycjimmy/semantic-release-action@v4 + uses: cycjimmy/semantic-release-action@v5 id: semantic env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }} KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }} - + - name: Attest if: steps.semantic.outputs.new_release_published == 'true' - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: - subject-path: build/app/outputs/apk/release/revanced-manager-*.apk + subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}' + subject-path: app/build/outputs/apk/release/revanced-manager*.apk diff --git a/.github/workflows/sync_crowdin.yml b/.github/workflows/sync_crowdin.yml deleted file mode 100644 index cf27665e05..0000000000 --- a/.github/workflows/sync_crowdin.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Sync Crowdin - -on: - workflow_dispatch: - schedule: - - cron: 00 12 * * 1 - push: - branches: dev - paths: - - assets/i18n/*.json - - assets/i18n/*.dart - - .github/workflows/sync_crowdin.yml - -jobs: - sync: - name: Sync - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: dev - fetch-depth: 0 - clean: true - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - cache: true - - - name: Sync translations from Crowdin - uses: crowdin/github-action@v2 - with: - config: crowdin.yml - upload_sources: true - upload_translations: false - download_translations: true - localization_branch_name: feat/translations - skip_ref_checkout: true - create_pull_request: true - pull_request_title: "chore: Sync translations" - pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)" - pull_request_base_branch_name: "dev" - commit_message: "chore: Sync translations" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - - name: Validation of synced translations - run: | - dart pub get - dart run slang validate - - - name: Normalization of Translation Strings - run: | - sudo chmod 766 assets/i18n/*.i18n.json - - dart run slang analyze - dart run slang clean - dart run slang normalize - - dart run slang - - cd assets/i18n - dart nuke.dart >> $GITHUB_STEP_SUMMARY - cd ../.. - - flutter analyze lib/gen/strings.g.dart --no-fatal-infos --no-fatal-warnings - - - name: Commit translations - run: | - git config --global user.name 'github-actions[bot]' - git config --global user.email 'github-actions[bot]@users.noreply.github.com' - sudo chown -R $USER:$USER .git - git commit -m "chore: Remove empty values from JSON" assets/i18n/*.i18n.json - git push origin HEAD:feat/translations diff --git a/.github/workflows/update_documentation.yml b/.github/workflows/update_documentation.yml index 541a7aa5b5..f259a4b833 100644 --- a/.github/workflows/update_documentation.yml +++ b/.github/workflows/update_documentation.yml @@ -16,4 +16,4 @@ jobs: token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} repository: revanced/revanced-documentation event-type: update-documentation - client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}' + client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 387727a0f7..40c3c574ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,64 +1,132 @@ -# Miscellaneous +### Java template +# Compiled class file *.class -*.lock + +# Log file *.log -*.pyc -*.swp -.buildlog/ -.history -# packages file containing multi-root paths -.packages.generated +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries -# IntelliJ related +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules *.iml *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format *.iws -.idea/ -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -**/generated_plugin_registrant.dart -.packages -.pub-preload-cache/ -.pub/ -build/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -.gradle/ -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - -# Coverage -coverage/ - -# Symbols -app.*.symbols - -# Obfuscation related -app.*.map.json +# IntelliJ +out/ -# Generated files -android/app/.cxx -**/*.g.dart -**/*.locator.dart -**/*.router.dart +# mpeltonen/sbt-idea plugin +.idea_modules/ -# Project specific -node_modules/ +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# Potentially copyrighted test APK +*.apk + +# Ignore vscode config .vscode/ +# Dependency directories +node_modules/ + +# Ignore IDEA files +.idea/ + +.kotlin/ + +local.properties +.cxx \ No newline at end of file diff --git a/.metadata b/.metadata deleted file mode 100644 index c689480b51..0000000000 --- a/.metadata +++ /dev/null @@ -1,30 +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 and should not be manually edited. - -version: - revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - platform: android - create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 - - # 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 index 5ceed3a89d..40b3142abf 100644 --- a/.releaserc +++ b/.releaserc @@ -14,27 +14,17 @@ ] } ], - "@semantic-release/changelog", "@semantic-release/release-notes-generator", - [ - "semantic-release-pub", - { - "publishPub": false, - "updateBuildNumber": true - } - ], - [ - "@semantic-release/exec", - { - "prepareCmd": "flutter build apk" - } - ], + "@semantic-release/changelog", + "gradle-semantic-release-plugin", [ "@semantic-release/git", { "assets": [ - "pubspec.yaml" - ] + "CHANGELOG.md", + "gradle.properties", + ], + "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], [ @@ -42,20 +32,17 @@ { "assets": [ { - "path": "build/app/outputs/apk/release/revanced-manager*.apk" + "path": "app/build/outputs/apk/release/revanced-manager*.apk?(.asc)" }, ], - "commits": [ - "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - ], - "successComment": false + successComment: false } ], [ "@saithodev/semantic-release-backmerge", { - "backmergeBranches": [{"from": "main", "to": "dev"}], - "clearWorkspace": true + backmergeBranches: [{"from": "main", "to": "dev"}], + clearWorkspace: true } ] ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5841049990..f2838ff829 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ If you encounter a bug while using ReVanced Manager, open an issue using the ## 🤚 I want to contribute but don't know how to code -Even if you don't know how to code, you can still contribute by +Even if you don't know how to code, you can still contribute by translating ReVanced Manager on [Crowdin](https://translate.revanced.app/). ❤️ Thank you for considering contributing to ReVanced Manager, diff --git a/README.md b/README.md index 3b1b604fa9..91e70d8ce1 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,9 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c Some of the features ReVanced Manager provides are: -- 💉 **Patch apps**: Apply any patch of your choice to Android apps -- 📱 **Portable**: ReVanced Patcher that fits in your pocket -- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager -- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more +- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system +- 💉 **Patch**: Select and apply patches to any Android app +- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings ## 🔽 Download diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 15e3660da6..0000000000 --- a/SECURITY.md +++ /dev/null @@ -1,77 +0,0 @@ -

- - - - -
- - - - - -     - - - - - -     - - - - - -     - - - - - -     - - - - - -     - - - - - -     - - - - - - -
-
- 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 03e6e65932..0000000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,156 +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 - - lib/gen/ - -linter: - rules: - - always_declare_return_types - - require_trailing_commas - - always_put_control_body_on_new_line - - 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_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_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_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_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 - - provide_deprecation_message - - recursive_getters - - sized_box_for_whitespace - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - - sort_pub_dependencies - - 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 - - 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 55afd919c6..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/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 289feb1c3a..0000000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,104 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "app.revanced.manager.flutter" - compileSdk = 35 - - compileOptions { - isCoreLibraryDesugaringEnabled = true - - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - applicationId = "app.revanced.manager.flutter" - minSdk = 26 - targetSdk = 35 - versionCode = flutter.versionCode - versionName = flutter.versionName - - resValue("string", "app_name", "ReVanced Manager") - } - - applicationVariants.all { - outputs.all { - this as com.android.build.gradle.internal.api.ApkVariantOutputImpl - - outputFileName = "revanced-manager-$versionName.apk" - } - } - - buildTypes { - configureEach { - isShrinkResources = false - isMinifyEnabled = false - - signingConfig = signingConfigs["debug"] - - ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") - } - - release { - isShrinkResources = true - isMinifyEnabled = true - - val keystoreFile = file("keystore.jks") - if (keystoreFile.exists()) { - signingConfig = signingConfigs.create("release") { - storeFile = keystoreFile - storePassword = System.getenv("KEYSTORE_PASSWORD") - keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS") - keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD") - } - - resValue("string", "app_name", "ReVanced Manager") - } else { - applicationIdSuffix = ".development" - resValue("string", "app_name", "ReVanced Manager (Development)") - signingConfig = signingConfigs["debug"] - } - } - - debug { - applicationIdSuffix = ".debug" - resValue("string", "app_name", "ReVanced Manager (Debug)") - } - - named("profile") { - initWith(getByName("debug")) - applicationIdSuffix = ".profile" - resValue("string", "app_name", "ReVanced Manager (Profile)") - } - } - - packaging { - jniLibs { - useLegacyPackaging = true - excludes.add("/prebuilt/**") - } - - resources { - excludes.add("/prebuilt/**") - } - } -} - -flutter { - source = "../.." -} - -dependencies { - coreLibraryDesugaring(libs.desugar.jdk.libs) // https://pub.dev/packages/flutter_local_notifications#gradle-setup - implementation(libs.revanced.patcher) - implementation(libs.revanced.library) -} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro deleted file mode 100644 index fb7c0bf92b..0000000000 --- a/android/app/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ --dontobfuscate - --keep class app.revanced.** { *; } --keep class com.android.tools.smali.** { *; } --keep class kotlin.** { *; } --keep class com.google.auto.value.** { *; } --keep class com.android.apksig.internal.** { *; } --keepnames class com.google.common.collect.** --keepnames class org.xmlpull.** { *; } - --dontwarn com.google.auto.value.** --dontwarn com.google.j2objc.annotations.* --dontwarn java.awt.** --dontwarn javax.** - -# Required for Share Plus, ref: ReVanced/revanced-manager#2474 --keep interface android.content.res.XmlResourceParser { *; } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981d5..0000000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 52ffb5eefb..0000000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/ExportSettingsActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/ExportSettingsActivity.kt deleted file mode 100644 index 4290cbcc31..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/ExportSettingsActivity.kt +++ /dev/null @@ -1,81 +0,0 @@ -package app.revanced.manager.flutter - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import android.util.Base64 -import org.json.JSONObject -import java.io.ByteArrayInputStream -import java.io.File -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.MessageDigest - -class ExportSettingsActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (getFingerprint(callingPackage!!) == getFingerprint(packageName)) { - // Create JSON Object - val json = JSONObject() - - // Default Data - json.put("keystorePassword", "s3cur3p@ssw0rd") - - // Load Shared Preferences - val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - val allEntries: Map = sharedPreferences.getAll() - for ((key, value) in allEntries.entries) { - json.put(key.replace("flutter.", ""), value) - } - - // Load keystore - val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore") - if (keystoreFile.exists()) { - val keystoreBytes = keystoreFile.readBytes() - val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT) - json.put("keystore", keystoreBase64) - } - - // Load saved patches - val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json") - if (storedPatchesFile.exists()) { - val patchesBytes = storedPatchesFile.readBytes() - val patches = String(patchesBytes, Charsets.UTF_8) - json.put("patches", JSONObject(patches)) - } - - // Send data back - val resultIntent = Intent() - resultIntent.putExtra("data", json.toString()) - setResult(Activity.RESULT_OK, resultIntent) - finish() - } else { - val resultIntent = Intent() - setResult(Activity.RESULT_CANCELED) - finish() - } - } - - fun getFingerprint(packageName: String): String { - // Get the signature of the app that matches the package name - val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) - val signature = packageInfo.signatures!![0] - - // Get the raw certificate data - val rawCert = signature.toByteArray() - - // Generate an X509Certificate from the data - val certFactory = CertificateFactory.getInstance("X509") - val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate - - // Get the SHA256 fingerprint - val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") { - "%02x".format(it) - } - - return fingerprint - } -} 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 757eeb0cff..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt +++ /dev/null @@ -1,427 +0,0 @@ -package app.revanced.manager.flutter - -import android.app.PendingIntent -import android.app.SearchManager -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import android.os.Handler -import android.os.Looper -import app.revanced.library.ApkUtils -import app.revanced.library.ApkUtils.applyTo -import app.revanced.manager.flutter.utils.Aapt -import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver -import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver -import app.revanced.patcher.Patcher -import app.revanced.patcher.PatcherConfig -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.loadPatchesFromDex -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.io.PrintWriter -import java.io.StringWriter -import java.util.logging.LogRecord -import java.util.logging.Logger - - -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 - - private lateinit var patches: Set> - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - - val patcherChannel = "app.revanced.manager.flutter/patcher" - val installerChannel = "app.revanced.manager.flutter/installer" - val openBrowserChannel = "app.revanced.manager.flutter/browser" - - MethodChannel( - flutterEngine.dartExecutor.binaryMessenger, - openBrowserChannel - ).setMethodCallHandler { call, result -> - if (call.method == "openBrowser") { - val searchQuery = call.argument("query") - openBrowser(searchQuery) - result.success(null) - } else { - result.notImplemented() - } - } - - val mainChannel = - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel) - - this.installerChannel = - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel) - - mainChannel.setMethodCallHandler { call, result -> - when (call.method) { - "runPatcher" -> { - val inFilePath = call.argument("inFilePath") - val outFilePath = call.argument("outFilePath") - val selectedPatches = call.argument>("selectedPatches") - val options = call.argument>>("options") - val tmpDirPath = call.argument("tmpDirPath") - val keyStoreFilePath = call.argument("keyStoreFilePath") - val keystorePassword = call.argument("keystorePassword") - - if ( - inFilePath != null && - outFilePath != null && - selectedPatches != null && - options != null && - tmpDirPath != null && - keyStoreFilePath != null && - keystorePassword != null - ) { - cancel = false - runPatcher( - result, - inFilePath, - outFilePath, - selectedPatches, - options, - tmpDirPath, - keyStoreFilePath, - keystorePassword - ) - } else result.error( - "INVALID_ARGUMENTS", - "Invalid arguments", - "One or more arguments are missing" - ) - } - - "stopPatcher" -> { - cancel = true - stopResult = result - } - - "getPatches" -> { - val patchBundleFilePath = call.argument("patchBundleFilePath")!! - - try { - val patchBundleFile = File(patchBundleFilePath) - patchBundleFile.setWritable(false) - patches = loadPatchesFromDex( - setOf(patchBundleFile), - optimizedDexDirectory = codeCacheDir - ) - } catch (t: Throwable) { - return@setMethodCallHandler result.error( - "PATCH_BUNDLE_ERROR", - "Failed to load patch bundle", - t.stackTraceToString() - ) - } - - JSONArray().apply { - patches.forEach { - JSONObject().apply { - put("name", it.name) - put("description", it.description) - put("excluded", !it.use) - put("compatiblePackages", JSONArray().apply { - it.compatiblePackages?.forEach { (name, versions) -> - val compatiblePackageJson = JSONObject().apply { - put("name", name) - put( - "versions", - JSONArray().apply { - versions?.forEach { version -> - put(version) - } - }) - } - put(compatiblePackageJson) - } - }) - put("options", JSONArray().apply { - it.options.values.forEach { option -> - JSONObject().apply { - put("key", option.key) - put("title", option.title) - put("description", option.description) - put("required", option.required) - - fun JSONObject.putValue( - value: Any?, - key: String = "value" - ) = if (value is Array<*>) put( - key, - JSONArray().apply { - value.forEach { put(it) } - }) - else put(key, value) - - putValue(option.default) - - option.values?.let { values -> - put( - "values", - JSONObject().apply { - values.forEach { (key, value) -> - putValue(value, key) - } - }) - } ?: put("values", null) - put("type", option.type) - }.let(::put) - } - }) - }.let(::put) - } - }.toString().let(result::success) - } - - "installApk" -> { - val apkPath = call.argument("apkPath")!! - PackageInstallerManager.result = result - installApk(apkPath) - } - - "uninstallApp" -> { - val packageName = call.argument("packageName")!! - uninstallApp(packageName) - PackageInstallerManager.result = result - } - - else -> result.notImplemented() - } - } - } - - private fun openBrowser(query: String?) { - val intent = Intent(Intent.ACTION_WEB_SEARCH).apply { - putExtra(SearchManager.QUERY, query) - } - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } - } - - private fun runPatcher( - result: MethodChannel.Result, - inFilePath: String, - outFilePath: String, - selectedPatches: List, - options: Map>, - tmpDirPath: String, - keyStoreFilePath: String, - keystorePassword: String - ) { - val inFile = File(inFilePath) - // Necessary because the file is copied from a nonwriteable location. - inFile.setWritable(true) - inFile.setReadable(true) - val outFile = File(outFilePath) - val keyStoreFile = File(keyStoreFilePath) - val tmpDir = File(tmpDirPath) - - Thread { - fun updateProgress(progress: Double, header: String, log: String) { - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to progress, - "header" to header, - "log" to log - ) - ) - } - } - - fun postStop() = handler.post { stopResult!!.success(null) } - - fun cancel(block: () -> Unit = {}): Boolean { - if (cancel) { - block() - postStop() - } - - return cancel - } - - - // Setup logger - Logger.getLogger("").apply { - handlers.forEach { handler -> - handler.close() - removeHandler(handler) - } - - object : java.util.logging.Handler() { - override fun publish(record: LogRecord) { - if (cancel) return - if ( - record.loggerName?.startsWith("app.revanced") == true || - // Logger in class brut.util.OS. - record.loggerName == "" - ) updateProgress(-1.0, "", record.message) - } - - override fun flush() = Unit - override fun close() = flush() - }.let(::addHandler) - } - - try { - updateProgress(0.0, "Reading APK...", "Reading APK") - - val patcher = Patcher( - PatcherConfig( - inFile, - tmpDir, - Aapt.binary(applicationContext).absolutePath, - tmpDir.path, - ) - ) - - if (cancel(patcher::close)) return@Thread - updateProgress(0.02, "Loading patches...", "Loading patches") - - val patches = patches.filter { patch -> - val isCompatible = patch.compatiblePackages?.any { (name, _) -> - name == patcher.context.packageMetadata.packageName - } ?: false - - val compatibleOrUniversal = - isCompatible || patch.compatiblePackages.isNullOrEmpty() - - compatibleOrUniversal && selectedPatches.any { it == patch.name } - }.onEach { patch -> - options[patch.name]?.forEach { (key, value) -> - patch.options[key] = value - } - }.toSet() - - if (cancel(patcher::close)) return@Thread - updateProgress(0.05, "Executing...", "") - - val patcherResult = patcher.use { - it += patches - - runBlocking { - // Update the progress bar every time a patch is executed from 0.15 to 0.7 - val totalPatchesCount = patches.size - val progressStep = 0.55 / totalPatchesCount - var progress = 0.05 - - patcher().collect(FlowCollector { patchResult: PatchResult -> - if (cancel(patcher::close)) return@FlowCollector - - val msg = patchResult.exception?.let { - val writer = StringWriter() - it.printStackTrace(PrintWriter(writer)) - "${patchResult.patch.name} failed: $writer" - } ?: run { - "${patchResult.patch.name} succeeded" - } - - updateProgress(progress, "", msg) - progress += progressStep - }) - } - - if (cancel(patcher::close)) return@Thread - updateProgress(0.75, "Building...", "") - - patcher.get() - } - - if (cancel(patcher::close)) return@Thread - - patcherResult.applyTo(inFile) - - if (cancel(patcher::close)) return@Thread - - ApkUtils.signApk( - inFile, - outFile, - "ReVanced", - ApkUtils.KeyStoreDetails( - keyStoreFile, - keystorePassword, - "alias", - keystorePassword - ) - ) - - updateProgress(.85, "Patched", "Patched APK") - } catch (ex: Throwable) { - if (!cancel) { - val stack = ex.stackTraceToString() - updateProgress( - -100.0, - "Failed", - "An error occurred:\n$stack" - ) - } - } finally { - inFile.delete() - tmpDir.deleteRecursively() - } - - handler.post { result.success(null) } - }.start() - } - - private fun installApk(apkPath: String) { - val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller - val sessionParams = - PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) - val sessionId: Int = packageInstaller.createSession(sessionParams) - val session: PackageInstaller.Session = packageInstaller.openSession(sessionId) - session.use { activeSession -> - val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1) - sessionOutputStream.use { outputStream -> - val apkFile = File(apkPath) - apkFile.inputStream().use { inputStream -> - inputStream.copyTo(outputStream) - } - } - } - val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply { - action = "APP_INSTALL_ACTION" - } - val receiverPendingIntent = PendingIntent.getBroadcast( - context, - sessionId, - receiverIntent, - PackageInstallerManager.flags - ) - session.commit(receiverPendingIntent.intentSender) - session.close() - } - - private fun uninstallApp(packageName: String) { - val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller - val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply { - action = "APP_UNINSTALL_ACTION" - } - val receiverPendingIntent = - PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags) - packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender) - } - - object PackageInstallerManager { - var result: MethodChannel.Result? = null - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - } -} 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/packageInstaller/InstallerReceiver.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/InstallerReceiver.kt deleted file mode 100644 index d14a9daa5e..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/InstallerReceiver.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.revanced.manager.flutter.utils.packageInstaller - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import app.revanced.manager.flutter.MainActivity - -class InstallerReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) - if (confirmationIntent != null) { - context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } - } - - else -> { - val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME) - MainActivity.PackageInstallerManager.result!!.success(mapOf( - "status" to status, - "packageName" to packageName, - "message" to message, - "otherPackageName" to otherPackageName - )) - } - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt deleted file mode 100644 index 84dec3cca9..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/packageInstaller/UninstallerReceiver.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.revanced.manager.flutter.utils.packageInstaller - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import app.revanced.manager.flutter.MainActivity - -class UninstallerReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) - if (confirmationIntent != null) { - context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } - } - - else -> { - MainActivity.PackageInstallerManager.result!!.success(status) - } - } - } -} \ No newline at end of file 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/raw/revanced_manager_keep.xml b/android/app/src/main/res/raw/revanced_manager_keep.xml deleted file mode 100644 index 078ab00912..0000000000 --- a/android/app/src/main/res/raw/revanced_manager_keep.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file 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 399f6981d5..0000000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/build.gradle.kts b/android/build.gradle.kts deleted file mode 100644 index d0a3415f4d..0000000000 --- a/android/build.gradle.kts +++ /dev/null @@ -1,40 +0,0 @@ -import com.android.build.api.dsl.CommonExtension -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -allprojects { - repositories { - google() - mavenCentral() - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/revanced/registry") - credentials { - username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") - password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") - } - } - } -} - -layout.buildDirectory = File("../build") - -project(":screenshot_callback") { - tasks.withType().configureEach { - kotlinOptions { - jvmTarget = "17" - } - } -} - -subprojects { - afterEvaluate { - extensions.findByName("android")?.let { - it as CommonExtension<*, *, *, *, *, *> - if (it.compileSdk != null && it.compileSdk!! < 31) - it.compileSdk = 34 - } - } - - layout.buildDirectory = rootProject.layout.buildDirectory.file(name).get().asFile - evaluationDependsOn(":app") -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 7676bdfcbc..0000000000 --- a/android/gradle.properties +++ /dev/null @@ -1,7 +0,0 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -org.gradle.parallel=true -org.gradle.daemon=true -org.gradle.caching=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml deleted file mode 100644 index acac669cd5..0000000000 --- a/android/gradle/libs.versions.toml +++ /dev/null @@ -1,9 +0,0 @@ -[versions] -revanced-patcher = "21.0.0" -revanced-library = "3.1.0" -desugar_jdk_libs = "2.1.4" - -[libraries] -revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } -revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" } -desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts deleted file mode 100644 index 261ac826a5..0000000000 --- a/android/settings.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -pluginManagement { - val properties = java.util.Properties().apply { - load(file("local.properties").inputStream()) - } - - val flutterSdkPath = properties.getProperty("flutter.sdk") - assert(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.9.0" apply false - id("org.jetbrains.kotlin.android") version "2.1.10" apply false -} - -include(":app") diff --git a/api/api/api.api b/api/api/api.api new file mode 100644 index 0000000000..eccdb145eb --- /dev/null +++ b/api/api/api.api @@ -0,0 +1,182 @@ +public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope { +} + +public final class app/revanced/manager/plugin/downloader/ConstantsKt { + public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String; +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getHeaders ()Ljava/util/Map; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public final fun toDownloadResult ()Lkotlin/Pair; + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Downloader { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/DownloaderKt { + public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; +} + +public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope { + public static final field $stable I + public final fun download (Lkotlin/jvm/functions/Function3;)V + public final fun get (Lkotlin/jvm/functions/Function4;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/ExtensionsKt { + public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V +} + +public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { +} + +public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { + public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation { +} + +public abstract interface class app/revanced/manager/plugin/downloader/Scope { + public abstract fun getHostPackageName ()Ljava/lang/String; + public abstract fun getPluginPackageName ()Ljava/lang/String; +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception { + public static final field $stable I + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException { + public static final field $stable I + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { + public static final field $stable I + public final fun getIntent ()Landroid/content/Intent; + public final fun getResultCode ()I +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException { + public static final field $stable I +} + +public final class app/revanced/manager/plugin/downloader/webview/APIKt { + public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; + public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public fun finish ()V + public fun load (Ljava/lang/String;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun pageLoad (Ljava/lang/String;)V + public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope { + public static final field $stable I + public final fun download (Lkotlin/jvm/functions/Function5;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V +} + diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000000..a784968c8b --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,153 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.io.IOException + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.binary.compatibility.validator) + `maven-publish` + signing +} + +group = "app.revanced" + +dependencies { + implementation(libs.androidx.ktx) + implementation(libs.runtime.ktx) + implementation(libs.activity.compose) + implementation(libs.appcompat) +} + +fun String.runCommand(): String { + val process = ProcessBuilder(split("\\s".toRegex())) + .redirectErrorStream(true) + .directory(rootDir) + .start() + + val output = StringBuilder() + val reader = process.inputStream.bufferedReader() + + val thread = Thread { + reader.forEachLine { + output.appendLine(it) + } + } + thread.start() + + if (!process.waitFor(10, TimeUnit.SECONDS)) { + process.destroy() + throw IOException("Command timed out: $this") + } + + thread.join() + return output.toString().trim() +} + +val projectPath: String = projectDir.relativeTo(rootDir).path +val lastTag = "git describe --tags --abbrev=0".runCommand() +val hasChangesInThisModule = "git diff --name-only $lastTag..HEAD".runCommand().lineSequence().any { + it.startsWith(projectPath) +} + +tasks.matching { it.name.startsWith("publish") }.configureEach { + onlyIf { + hasChangesInThisModule + } +} + +android { + namespace = "app.revanced.manager.plugin.downloader" + compileSdk = 36 + + defaultConfig { + minSdk = 26 + + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget("17") + } + } + + buildFeatures { + aidl = true + } +} + +apiValidation { + nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi" +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-manager") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String? + password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String? + } + } + } + + publications { + create("Api") { + afterEvaluate { + from(components["release"]) + } + + groupId = "app.revanced" + artifactId = "revanced-manager-api" + version = project.version.toString() + + pom { + name = "ReVanced Manager API" + description = "API for ReVanced Manager." + url = "https://revanced.app" + + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-manager.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-manager.git" + url = "https://github.com/revanced/revanced-manager" + } + } + } + } +} + +signing { + useGpgCmd() + sign(publishing.publications["Api"]) +} \ No newline at end of file diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..74b7379f73 --- /dev/null +++ b/api/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl new file mode 100644 index 0000000000..d657fcc3c3 --- /dev/null +++ b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl @@ -0,0 +1,8 @@ +// IWebView.aidl +package app.revanced.manager.plugin.downloader.webview; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebView { + void load(String url); + void finish(); +} \ No newline at end of file diff --git a/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl new file mode 100644 index 0000000000..b0237de2a7 --- /dev/null +++ b/api/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl @@ -0,0 +1,11 @@ +// IWebViewEvents.aidl +package app.revanced.manager.plugin.downloader.webview; + +import app.revanced.manager.plugin.downloader.webview.IWebView; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebViewEvents { + void ready(IWebView iface); + void pageLoad(String url); + void download(String url, String mimetype, String userAgent); +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt new file mode 100644 index 0000000000..469daaaec3 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Constants.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.plugin.downloader + +/** + * The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission. + * Plugin UI activities and internal services can be protected using this permission. + */ +const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST" \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt new file mode 100644 index 0000000000..bf0a219b58 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Downloader.kt @@ -0,0 +1,165 @@ +package app.revanced.manager.plugin.downloader + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.app.Activity +import android.os.Parcelable +import kotlinx.coroutines.withTimeout +import java.io.InputStream +import java.io.OutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This API is only intended for plugin hosts, don't use it in a plugin.", +) +@Retention(AnnotationRetention.BINARY) +annotation class PluginHostApi + +/** + * The base interface for all DSL scopes. + */ +interface Scope { + /** + * The package name of ReVanced Manager. + */ + val hostPackageName: String + + /** + * The package name of the plugin. + */ + val pluginPackageName: String +} + +/** + * The scope of [DownloaderScope.get]. + */ +interface GetScope : Scope { + /** + * Ask the user to perform some required interaction in the activity specified by the provided [Intent]. + * This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK]. + * + * @throws UserInteractionException.RequestDenied User decided to skip this plugin. + * @throws UserInteractionException.Activity.Cancelled The activity was cancelled. + * @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code. + */ + suspend fun requestStartActivity(intent: Intent): Intent? +} + +interface BaseDownloadScope : Scope + +/** + * The scope for [DownloaderScope.download]. + */ +interface InputDownloadScope : BaseDownloadScope + +typealias Size = Long +typealias DownloadResult = Pair + +typealias Version = String +typealias GetResult = Pair + +class DownloaderScope internal constructor( + private val scopeImpl: Scope, + internal val context: Context +) : Scope by scopeImpl { + // Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases. + // It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins. + internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null + internal var get: (suspend GetScope.(String, String?) -> GetResult?)? = null + private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {} + + /** + * Define the download block of the plugin. + */ + fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) { + download = { app, outputStream -> + val (inputStream, size) = inputDownloadScopeImpl.block(app) + + inputStream.use { + if (size != null) reportSize(size) + it.copyTo(outputStream) + } + } + } + + /** + * Define the get block of the plugin. + * The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null. + */ + fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) { + get = block + } + + /** + * Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends. + */ + suspend fun useService(intent: Intent, block: suspend (IBinder) -> R): R { + var onBind: ((IBinder) -> Unit)? = null + val serviceConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = + onBind!!(service!!) + + override fun onServiceDisconnected(name: ComponentName?) {} + } + + return try { + val binder = withTimeout(10000L) { + suspendCoroutine { continuation -> + onBind = continuation::resume + context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE) + } + } + block(binder) + } finally { + onBind = null + context.unbindService(serviceConn) + } + } +} + +class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { + @PluginHostApi + fun build(scopeImpl: Scope, context: Context) = + with(DownloaderScope(scopeImpl, context)) { + block() + + Downloader( + download = download!!, + get = get!! + ) + } +} + +class Downloader internal constructor( + @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult?, + @property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit +) + +/** + * Define a downloader plugin. + */ +fun Downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block) + +/** + * @see GetScope.requestStartActivity + */ +sealed class UserInteractionException(message: String) : Exception(message) { + class RequestDenied @PluginHostApi constructor() : + UserInteractionException("Request denied by user") + + sealed class Activity(message: String) : UserInteractionException(message) { + class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled") + + /** + * @param resultCode The result code of the activity. + * @param intent The [Intent] of the activity. + */ + class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) : + Activity("Unexpected activity result code: $resultCode") + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt new file mode 100644 index 0000000000..a1e6bf795b --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Extensions.kt @@ -0,0 +1,42 @@ +package app.revanced.manager.plugin.downloader + +import android.app.Activity +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.Parcelable +import java.io.OutputStream + +/** + * The scope of the [OutputStream] version of [DownloaderScope.download]. + */ +interface OutputDownloadScope : BaseDownloadScope { + suspend fun reportSize(size: Long) +} + +/** + * A replacement for [DownloaderScope.download] that uses [OutputStream]. + * The provided [OutputStream] does not need to be closed manually. + */ +fun DownloaderScope.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) { + download = block +} + +/** + * Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY]. + * @see [GetScope.requestStartActivity] + */ +suspend inline fun GetScope.requestStartActivity() = + requestStartActivity( + Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) } + ) + +/** + * Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE]. + * @see [DownloaderScope.useService] + */ +suspend inline fun DownloaderScope<*>.useService( + noinline block: suspend (IBinder) -> R +) = useService( + Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block +) \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt new file mode 100644 index 0000000000..414ad88947 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/Parcelables.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.plugin.downloader + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.net.HttpURLConnection +import java.net.URI + +/** + * A simple parcelable data class for storing a package name and version. + * This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function. + * + * @param name The package name. + * @param version The version. + */ +@Parcelize +data class Package(val name: String, val version: String) : Parcelable + +/** + * A data class for storing a download URL. + * + * @param url The download URL. + * @param headers The headers to use for the request. + */ +@Parcelize +data class DownloadUrl(val url: String, val headers: Map = emptyMap()) : Parcelable { + /** + * Converts this into a [DownloadResult]. + */ + fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) { + useCaches = false + allowUserInteraction = false + headers.forEach(::setRequestProperty) + + connectTimeout = 10_000 + connect() + + inputStream to getHeaderField("Content-Length").toLong() + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt new file mode 100644 index 0000000000..2e5034e189 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/API.kt @@ -0,0 +1,176 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.content.Intent +import app.revanced.manager.plugin.downloader.DownloadUrl +import app.revanced.manager.plugin.downloader.DownloaderScope +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.Scope +import app.revanced.manager.plugin.downloader.Downloader +import app.revanced.manager.plugin.downloader.PluginHostApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlin.properties.Delegates + +typealias InitialUrl = String +typealias PageLoadCallback = suspend WebViewCallbackScope.(url: String) -> Unit +typealias DownloadCallback = suspend WebViewCallbackScope.(url: String, mimeType: String, userAgent: String) -> Unit + +interface WebViewCallbackScope : Scope { + /** + * Finishes the activity and returns the [result]. + */ + suspend fun finish(result: T) + + /** + * Tells the WebView to load the specified [url]. + */ + suspend fun load(url: String) +} + +@OptIn(PluginHostApi::class) +class WebViewScope internal constructor( + coroutineScope: CoroutineScope, + private val scopeImpl: Scope, + setResult: (T) -> Unit +) : Scope by scopeImpl { + private var onPageLoadCallback: PageLoadCallback = {} + private var onDownloadCallback: DownloadCallback = { _, _, _ -> } + + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher = Dispatchers.Default.limitedParallelism(1) + private lateinit var webView: IWebView + internal lateinit var initialUrl: String + + internal val binder = object : IWebViewEvents.Stub() { + override fun ready(iface: IWebView?) { + coroutineScope.launch(dispatcher) { + webView = iface!!.also { + it.load(initialUrl) + } + } + } + + override fun pageLoad(url: String?) { + coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) } + } + + override fun download(url: String?, mimetype: String?, userAgent: String?) { + coroutineScope.launch(dispatcher) { + onDownloadCallback( + callbackScope, + url!!, + mimetype!!, + userAgent!! + ) + } + } + } + + private val callbackScope = object : WebViewCallbackScope, Scope by scopeImpl { + override suspend fun finish(result: T) { + setResult(result) + // Tell the WebViewActivity to finish + webView.let { withContext(Dispatchers.IO) { it.finish() } } + } + + override suspend fun load(url: String) { + webView.let { withContext(Dispatchers.IO) { it.load(url) } } + } + + } + + /** + * Called when the WebView attempts to download a file to disk. + */ + fun download(block: DownloadCallback) { + onDownloadCallback = block + } + + /** + * Called when the WebView finishes loading a page. + */ + fun pageLoad(block: PageLoadCallback) { + onPageLoadCallback = block + } +} + +@JvmInline +private value class Container(val value: U) + +/** + * Run a [android.webkit.WebView] Activity controlled by the provided code block. + * The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish]. + * The [block] defines the event handlers and returns the initial URL. + * + * @param title The string displayed in the action bar. + * @param block The control block. + */ +@OptIn(PluginHostApi::class) +suspend fun GetScope.runWebView( + title: String, + block: suspend WebViewScope.() -> InitialUrl +) = supervisorScope { + var result by Delegates.notNull>() + + val scope = WebViewScope(this@supervisorScope, this@runWebView) { result = Container(it) } + scope.initialUrl = scope.block() + + // Start the webview activity and wait until it finishes. + requestStartActivity(Intent().apply { + putExtra( + WebViewActivity.KEY, + WebViewActivity.Parameters(title, scope.binder) + ) + setClassName( + hostPackageName, + WebViewActivity::class.qualifiedName!! + ) + }) + + // Return the result and cancel any leftover coroutines. + coroutineContext.cancelChildren() + result.value +} + +/** + * Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView]. + * Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get]. + * + * @see runWebView + */ +fun WebViewDownloader(block: suspend WebViewScope.(packageName: String, version: String?) -> InitialUrl?) = + Downloader { + val label = context.applicationInfo.loadLabel( + context.packageManager + ).toString() + + get { packageName, version -> + class ReturnNull : Exception() + + try { + runWebView(label) { + download { url, _, userAgent -> + finish( + DownloadUrl( + url, + mapOf("User-Agent" to userAgent) + ) + ) + } + + block(this@runWebView, packageName, version) ?: throw ReturnNull() + } to version + } catch (_: ReturnNull) { + null + } + } + + download { + it.toDownloadResult() + } + } \ No newline at end of file diff --git a/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt new file mode 100644 index 0000000000..aff0133784 --- /dev/null +++ b/api/src/main/kotlin/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt @@ -0,0 +1,161 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.annotation.SuppressLint +import android.os.Bundle +import android.os.IBinder +import android.os.Parcelable +import android.view.MenuItem +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.viewModelScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.R +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@OptIn(PluginHostApi::class) +@PluginHostApi +class WebViewActivity : ComponentActivity() { + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val vm by viewModels() + enableEdgeToEdge() + setContentView(R.layout.activity_webview) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + val webView = findViewById(R.id.webview) + onBackPressedDispatcher.addCallback { + if (webView.canGoBack()) webView.goBack() + else cancelActivity() + } + + val params = intent.getParcelableExtra(KEY)!! + actionBar?.apply { + title = params.title + setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel) + setDisplayHomeAsUpEnabled(true) + } + + val events = IWebViewEvents.Stub.asInterface(params.events)!! + vm.setup(events) + + webView.apply { + settings.apply { + cacheMode = WebSettings.LOAD_NO_CACHE + allowContentAccess = false + domStorageEnabled = true + javaScriptEnabled = true + } + + webViewClient = vm.webViewClient + setDownloadListener { url, userAgent, _, mimetype, _ -> + vm.onDownload(url, mimetype, userAgent) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + vm.commands.collect { + when (it) { + is WebViewModel.Command.Finish -> { + setResult(RESULT_OK) + finish() + } + + is WebViewModel.Command.Load -> webView.loadUrl(it.url) + } + } + } + } + } + + private fun cancelActivity() { + setResult(RESULT_CANCELED) + finish() + } + + override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) { + cancelActivity() + + true + } else super.onOptionsItemSelected(item) + + @Parcelize + internal class Parameters( + val title: String, val events: IBinder + ) : Parcelable + + internal companion object { + const val KEY = "params" + } +} + +@OptIn(PluginHostApi::class) +internal class WebViewModel : ViewModel() { + init { + CookieManager.getInstance().apply { + removeAllCookies(null) + setAcceptCookie(true) + } + } + + private val commandChannel = Channel() + val commands = commandChannel.receiveAsFlow() + + private var eventBinder: IWebViewEvents? = null + private val ctrlBinder = object : IWebView.Stub() { + override fun load(url: String?) { + viewModelScope.launch { + commandChannel.send(Command.Load(url!!)) + } + } + + override fun finish() { + viewModelScope.launch { + commandChannel.send(Command.Finish) + } + } + } + + val webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + eventBinder!!.pageLoad(url) + } + } + + fun onDownload(url: String, mimeType: String, userAgent: String) { + eventBinder!!.download(url, mimeType, userAgent) + } + + fun setup(binder: IWebViewEvents) { + if (eventBinder != null) return + eventBinder = binder + binder.ready(ctrlBinder) + } + + sealed interface Command { + data class Load(val url: String) : Command + data object Finish : Command + } +} \ No newline at end of file diff --git a/api/src/main/res/layout/activity_webview.xml b/api/src/main/res/layout/activity_webview.xml new file mode 100644 index 0000000000..51f761d993 --- /dev/null +++ b/api/src/main/res/layout/activity_webview.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/api/src/main/res/values/strings.xml b/api/src/main/res/values/strings.xml new file mode 100644 index 0000000000..73862c416f --- /dev/null +++ b/api/src/main/res/values/strings.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/api/src/main/res/values/themes.xml b/api/src/main/res/values/themes.xml new file mode 100644 index 0000000000..495cde8e34 --- /dev/null +++ b/api/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file 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..dd1f59d60c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,253 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +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) + signing +} + +val outputApkFileName = "${rootProject.name}-$version.apk" + +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) + + // 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) + ksp(libs.room.compiler) + + // ReVanced + implementation(libs.revanced.patcher) + implementation(libs.revanced.library) + + // Downloader plugins + implementation(projects.api) + + // 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) +} + +android { + namespace = "app.revanced.manager" + compileSdk = 36 + buildToolsVersion = "36.0.0" + + defaultConfig { + applicationId = "app.revanced.manager" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "0.0.1" + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + configureEach { + } + debug { + applicationIdSuffix = ".debug" + resValue("string", "app_name", "ReVanced Manager (Debug)") + 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") + } + + val keystoreFile = file("keystore.jks") + + if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) { + applicationIdSuffix = ".debug_signed" + resValue("string", "app_name", "ReVanced Manager (Debug signed)") + signingConfig = signingConfigs.getByName("debug") + + isPseudoLocalesEnabled = true + } else { + signingConfig = signingConfigs.create("release") { + storeFile = keystoreFile + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS") + keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD") + } + } + + buildConfigField("long", "BUILD_ID", "0L") + } + } + + applicationVariants.all { + outputs.all { + this as com.android.build.gradle.internal.api.ApkVariantOutputImpl + + outputFileName = outputApkFileName + } + } + + 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") + } + + kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget("17") + jvmToolchain(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" + } + } +} + +tasks { + // Needed by gradle-semantic-release-plugin. + // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. + val publish by registering { + group = "publishing" + description = "Build the release APK" + + dependsOn("assembleRelease") + + val apk = project.layout.buildDirectory.file("outputs/apk/release/${outputApkFileName}") + val ascFile = apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") } + + inputs.file(apk).withPropertyName("inputApk") + outputs.file(ascFile).withPropertyName("outputAsc") + + doLast { + signing { + useGpgCmd() + sign(apk.get().asFile) + } + } + } +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000000..2fc38c4f19 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,14 @@ +-dontobfuscate + +-keep class app.revanced.manager.patcher.runtime.process.* { *; } +-keep class app.revanced.manager.plugin.** { *; } +-keep class app.revanced.patcher.** { *; } +-keep class com.android.tools.smali.** { *; } +-keep class kotlin.** { *; } +-keepnames class com.android.apksig.internal.** { *; } +-keepnames class org.xmlpull.** { *; } + +-dontwarn com.google.j2objc.annotations.* +-dontwarn java.awt.** +-dontwarn javax.** +-dontwarn org.slf4j.** \ 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..e418d68bd4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..c847260e36 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -0,0 +1,336 @@ +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.ContributorSettingsScreen +import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen +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.LicensesSettingsScreen +import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen +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 + } + } + }, + viewModel = 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() + }, + viewModel = 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 { + DeveloperSettingsScreen(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 { + ChangelogsSettingsScreen(onBackClick = navController::popBackStack) + } + + composable { + ContributorSettingsScreen(onBackClick = navController::popBackStack) + } + + composable { + LicensesSettingsScreen(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/redux/Redux.kt b/app/src/main/java/app/revanced/manager/data/redux/Redux.kt new file mode 100644 index 0000000000..785dedc454 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/redux/Redux.kt @@ -0,0 +1,74 @@ +package app.revanced.manager.data.redux + +import android.util.Log +import app.revanced.manager.util.tag +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull + +// This file implements React Redux-like state management. + +class Store(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext { + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + // Do not touch these without the lock. + private var isRunningActions = false + private val queueChannel = Channel>(capacity = 10) + private val lock = Mutex() + + suspend fun dispatch(action: Action) = lock.withLock { + Log.d(tag, "Dispatching $action") + queueChannel.send(action) + + if (isRunningActions) return@withLock + isRunningActions = true + coroutineScope.launch { + runActions() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun runActions() { + while (true) { + val action = withTimeoutOrNull(200L) { queueChannel.receive() } + if (action == null) { + Log.d(tag, "Stopping action runner") + lock.withLock { + // New actions may be dispatched during the timeout. + isRunningActions = !queueChannel.isEmpty + if (!isRunningActions) return + } + continue + } + + Log.d(tag, "Running $action") + _state.value = try { + with(action) { this@Store.execute(_state.value) } + } catch (c: CancellationException) { + // This is done without the lock, but cancellation usually means the store is no longer needed. + isRunningActions = false + throw c + } catch (e: Exception) { + action.catch(e) + continue + } + } + } +} + +interface ActionContext + +interface Action { + suspend fun ActionContext.execute(current: S): S + suspend fun catch(exception: Exception) { + Log.e(tag, "Got exception while executing $this", exception) + } +} \ 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..385133659c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -0,0 +1,30 @@ +package app.revanced.manager.data.room.bundles + +import androidx.room.* + +@Dao +interface PatchBundleDao { + @Query("SELECT * FROM patch_bundles") + suspend fun all(): List + + @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid") + suspend fun updateVersionHash(uid: Int, patches: String?) + + @Query("DELETE FROM patch_bundles WHERE uid != 0") + suspend fun purgeCustomBundles() + + @Transaction + suspend fun reset() { + purgeCustomBundles() + updateVersionHash(0, null) // Reset the main source + } + + @Query("DELETE FROM patch_bundles WHERE uid = :uid") + suspend fun remove(uid: Int) + + @Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid") + suspend fun getProps(uid: Int): PatchBundleProperties? + + @Upsert + suspend fun upsert(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..9119d50032 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -0,0 +1,46 @@ +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 versionHash: String? = null, + @ColumnInfo(name = "source") val source: Source, + @ColumnInfo(name = "auto_update") val autoUpdate: Boolean +) + +data class PatchBundleProperties( + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "version") val versionHash: String? = null, + @ColumnInfo(name = "source") val source: Source, + @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..66c69b43c1 --- /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