diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index bb66d8526..8d0fdcd93 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -25,7 +25,8 @@ jobs: - name: Build env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew :patches:buildAndroid --no-daemon - name: Upload artifacts diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml index be27b0687..2ef910457 100644 --- a/.github/workflows/pull_strings.yml +++ b/.github/workflows/pull_strings.yml @@ -2,7 +2,7 @@ name: Pull strings on: schedule: - - cron: "0 */12 * * *" + - cron: "0 0 * * 0" workflow_dispatch: jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 946dc9380..28f152e2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,8 @@ jobs: - name: Build env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew :patches:buildAndroid clean - name: Setup Node.js @@ -55,6 +56,8 @@ jobs: id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} - name: Attest if: steps.release.outputs.new_release_published == 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1303913..cbdc30d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,117 @@ +# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19) + + +### Features + +* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145)) + +# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19) + + +### Features + +* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48)) +* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f)) + +# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19) + + +### Features + +* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e)) + +# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19) + + +### Bug Fixes + +* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9)) +* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2)) + + +### Features + +* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307)) +* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44)) +* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8)) + +# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08) + + +### Features + +* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305)) +* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c)) + +# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04) + + +### Features + +* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc)) + +# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04) + + +### Features + +* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc)) +* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567)) + +# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04) + + +### Bug Fixes + +* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8)) + +# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30) + + +### Bug Fixes + +* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154)) + +# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29) + + +### Features + +* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9)) + +# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28) + + +### Bug Fixes + +* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c)) + + +### Features + +* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9)) + +# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27) + + +### Features + +* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc)) + +# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23) + + +### Bug Fixes + +* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1)) +* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515)) + + +### Features + +* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90)) +* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c)) + # [5.47.0](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0) (2025-12-18) diff --git a/crowdin.yml b/crowdin.yml index 148f321cd..81022c88c 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,9 @@ project_id_env: "CROWDIN_PROJECT_ID" api_token_env: "CROWDIN_PERSONAL_TOKEN" -preserve_hierarchy: false +preserve_hierarchy: true files: - source: patches/src/main/resources/addresources/values/strings.xml + dest: patches.xml translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml skip_untranslated_strings: true diff --git a/extensions/all/misc/disable-play-integrity/build.gradle.kts b/extensions/all/misc/disable-play-integrity/build.gradle.kts new file mode 100644 index 000000000..549297227 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/build.gradle.kts @@ -0,0 +1,20 @@ +android { + namespace = "app.revanced.extension" + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + buildFeatures { + aidl = true + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl new file mode 100644 index 000000000..7b8f59f1d --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +interface IExpressIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl new file mode 100644 index 000000000..624167afb --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl @@ -0,0 +1,5 @@ +package com.google.android.play.core.integrity.protocol; + +interface IExpressIntegrityServiceCallback { + oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl new file mode 100644 index 000000000..bb1bcd551 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +interface IIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl new file mode 100644 index 000000000..9485ec169 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; + +interface IIntegrityServiceCallback { + oneway void onResult(in Bundle result) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java new file mode 100644 index 000000000..31c2ca6db --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java @@ -0,0 +1,10 @@ +package android.ext; +/** @hide */ +// Int values that are assigned to packages in this interface can be retrieved at runtime from +// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server). +// +// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check +// or by a check that the APK is stored on an immutable OS partition. +public interface PackageId { + String PLAY_STORE_NAME = "com.android.vending"; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java new file mode 100644 index 000000000..a01806441 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java @@ -0,0 +1,62 @@ +package android.os; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.FileDescriptor; + +/** @hide */ +public class BinderWrapper implements IBinder { + protected final IBinder base; + + public BinderWrapper(IBinder base) { + this.base = base; + } + + @Override + public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + return base.transact(code, data, reply, flags); + } + + @Nullable + @Override + public IInterface queryLocalInterface(@NonNull String descriptor) { + return base.queryLocalInterface(descriptor); + } + + @Nullable + @Override + public String getInterfaceDescriptor() throws RemoteException { + return base.getInterfaceDescriptor(); + } + + @Override + public boolean pingBinder() { + return base.pingBinder(); + } + + @Override + public boolean isBinderAlive() { + return base.isBinderAlive(); + } + + @Override + public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dump(fd, args); + } + + @Override + public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dumpAsync(fd, args); + } + + @Override + public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException { + base.linkToDeath(recipient, flags); + } + + @Override + public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { + return base.unlinkToDeath(recipient, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..3bd88d2a6 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java @@ -0,0 +1,41 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IIntegrityService; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + ClassicPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.e("IIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..0418b4fe7 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java @@ -0,0 +1,48 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.BinderWrapper; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.Nullable; + +abstract class PlayIntegrityServiceWrapper extends BinderWrapper { + final String TAG; + protected int requestIntegrityTokenTxnCode; + + public PlayIntegrityServiceWrapper(IBinder base) { + super(base); + TAG = getClass().getSimpleName(); + } + + protected abstract Binder createTokenRequestStub(); + + @Override + public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + if (code == requestIntegrityTokenTxnCode) { + if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) { + return true; + } + } + return super.transact(code, data, reply, flags); + } + + private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) { + Log.d(TAG, "integrity token request detected"); + + try { + createTokenRequestStub().transact(code, data, reply, flags); + } catch (RemoteException e) { + // this is a local call + throw new IllegalStateException(e); + } + return true; + } + + protected static long getTokenRequestResultDelay() { + return 500L; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java new file mode 100644 index 000000000..6ff4720cc --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java @@ -0,0 +1,35 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.content.Intent; +import android.content.ServiceConnection; +import android.ext.PackageId; +import android.os.IBinder; +import androidx.annotation.Nullable; +import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper; +import java.util.function.UnaryOperator; + +public class PlayIntegrityUtils { + + public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) { + if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) { + UnaryOperator binderOverride = null; + + final String CLASSIC_SERVICE = + "com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE"; + final String STANDARD_SERVICE = + "com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE"; + + String action = service.getAction(); + if (STANDARD_SERVICE.equals(action)) { + binderOverride = StandardPlayIntegrityServiceWrapper::new; + } else if (CLASSIC_SERVICE.equals(action)) { + binderOverride = ClassicPlayIntegrityServiceWrapper::new; + } + + if (binderOverride != null) { + return new ServiceConnectionWrapper(orig, binderOverride); + } + } + return null; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java new file mode 100644 index 000000000..c1c4937f0 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java @@ -0,0 +1,42 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.annotation.SuppressLint; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityService; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +@SuppressLint("LongLogTag") +class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + StandardPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IExpressIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onRequestExpressIntegrityTokenResult(result); + } catch (RemoteException e) { + Log.e("IExpressIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java new file mode 100644 index 000000000..9edfc39f8 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java @@ -0,0 +1,49 @@ +package app.grapheneos.gmscompat.lib.util; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; + +import java.util.function.UnaryOperator; + +public class ServiceConnectionWrapper implements ServiceConnection { + private final ServiceConnection base; + private final UnaryOperator binderOverride; + + public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator binderOverride) { + this.base = base; + this.binderOverride = binderOverride; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + IBinder override = binderOverride.apply(service); + if (override != null) { + service = override; + } + } + + base.onServiceConnected(name, service); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + base.onServiceDisconnected(name); + } + + @Override + public void onBindingDied(ComponentName name) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + base.onBindingDied(name); + } + } + + @Override + public void onNullBinding(ComponentName name) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + base.onNullBinding(name); + } + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java new file mode 100644 index 000000000..a27e56be9 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/playintegrity/DisablePlayIntegrityPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.playintegrity; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils; + +public class DisablePlayIntegrityPatch { + public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) { + ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn); + if (override != null) { + conn = override; + } + + return context.bindService(service, conn, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java new file mode 100644 index 000000000..6b4cb92b4 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java @@ -0,0 +1,11 @@ +package com.android.internal.os; + +import android.os.Handler; +import android.os.Looper; + +public class FakeBackgroundHandler { + + public static Handler getHandler() { + return new Handler(Looper.getMainLooper()); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 5d6bb492d..ea37cfcde 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -311,6 +311,10 @@ public class Utils { return resourceId; } + public static String getResourceString(int id) throws Resources.NotFoundException { + return getContext().getResources().getString(id); + } + public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer")); } diff --git a/extensions/strava/build.gradle.kts b/extensions/strava/build.gradle.kts new file mode 100644 index 000000000..f282f41ea --- /dev/null +++ b/extensions/strava/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:strava:stub")) + compileOnly(libs.okhttp) +} diff --git a/extensions/strava/src/main/AndroidManifest.xml b/extensions/strava/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/strava/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java b/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java new file mode 100644 index 000000000..4c57cc792 --- /dev/null +++ b/extensions/strava/src/main/java/app/revanced/extension/strava/AddMediaDownloadPatch.java @@ -0,0 +1,216 @@ +package app.revanced.extension.strava; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.webkit.MimeTypeMap; + +import com.strava.core.data.MediaType; +import com.strava.photos.data.Media; + +import okhttp3.*; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import app.revanced.extension.shared.Utils; + +@SuppressLint("NewApi") +public final class AddMediaDownloadPatch { + public static final int ACTION_DOWNLOAD = -1; + public static final int ACTION_OPEN_LINK = -2; + public static final int ACTION_COPY_LINK = -3; + + private static final OkHttpClient client = new OkHttpClient(); + + public static boolean handleAction(int actionId, Media media) { + String url = getUrl(media); + switch (actionId) { + case ACTION_DOWNLOAD: + String name = media.getId(); + if (media.getType() == MediaType.VIDEO) { + downloadVideo(url, name); + } else { + downloadPhoto(url, name); + } + return true; + case ACTION_OPEN_LINK: + Utils.openLink(url); + return true; + case ACTION_COPY_LINK: + copyLink(url); + return true; + default: + return false; + } + } + + public static void copyLink(CharSequence url) { + Utils.setClipboard(url); + showInfoToast("link_copied_to_clipboard", "🔗"); + } + + public static void downloadPhoto(String url, String name) { + showInfoToast("loading", "⏳"); + Utils.runOnBackgroundThread(() -> { + try (Response response = fetch(url)) { + ResponseBody body = response.body(); + String mimeType = body.contentType().toString(); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + ContentResolver resolver = Utils.getContext().getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.DISPLAY_NAME, name + '.' + extension); + values.put(MediaStore.Images.Media.IS_PENDING, 1); + values.put(MediaStore.Images.Media.MIME_TYPE, mimeType); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Strava"); + Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) + : MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + Uri row = resolver.insert(collection, values); + try (OutputStream outputStream = resolver.openOutputStream(row)) { + transferTo(body.byteStream(), outputStream); + } finally { + values.clear(); + values.put(MediaStore.Images.Media.IS_PENDING, 0); + resolver.update(row, values, null); + } + showInfoToast("yis_2024_local_save_image_success", "✔️"); + } catch (IOException e) { + showErrorToast("download_failure", "❌", e); + } + }); + } + + /** + * Downloads a video in the M3U8 / HLS (HTTP Live Streaming) format. + */ + public static void downloadVideo(String url, String name) { + // The first request yields multiple URLs with different stream options. + // In case of Strava, the first one is always of highest quality. + // Each stream can consist of multiple chunks. + // The second request yields the URLs of all of these chunks. + // Fetch all of them concurrently and pipe their streams into the file in order. + showInfoToast("loading", "⏳"); + Utils.runOnBackgroundThread(() -> { + try { + String highestQualityStreamUrl; + try (Response response = fetch(url)) { + highestQualityStreamUrl = replaceFileName(url, lines(response).findFirst().get()); + } + List> futures; + try (Response response = fetch(highestQualityStreamUrl)) { + futures = lines(response) + .map(line -> replaceFileName(highestQualityStreamUrl, line)) + .map(chunkUrl -> Utils.submitOnBackgroundThread(() -> fetch(chunkUrl))) + .collect(Collectors.toList()); + } + ContentResolver resolver = Utils.getContext().getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Video.Media.DISPLAY_NAME, name + '.' + "mp4"); + values.put(MediaStore.Video.Media.IS_PENDING, 1); + values.put(MediaStore.Video.Media.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4")); + values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/Strava"); + Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) + : MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + Uri row = resolver.insert(collection, values); + try (OutputStream outputStream = resolver.openOutputStream(row)) { + Throwable error = null; + for (Future future : futures) { + if (error != null) { + if (future.cancel(true)) { + continue; + } + } + try (Response response = future.get()) { + if (error == null) { + transferTo(response.body().byteStream(), outputStream); + } + } catch (InterruptedException | IOException e) { + error = e; + } catch (ExecutionException e) { + error = e.getCause(); + } + } + if (error != null) { + throw new IOException(error); + } + } finally { + values.clear(); + values.put(MediaStore.Video.Media.IS_PENDING, 0); + resolver.update(row, values, null); + } + showInfoToast("yis_2024_local_save_video_success", "✔️"); + } catch (IOException e) { + showErrorToast("download_failure", "❌", e); + } + }); + } + + private static String getUrl(Media media) { + return media.getType() == MediaType.VIDEO + ? ((Media.Video) media).getVideoUrl() + : media.getLargestUrl(); + } + + private static String getString(String name, String fallback) { + int id = Utils.getResourceIdentifier(name, "string"); + return id != 0 + ? Utils.getResourceString(id) + : fallback; + } + + private static void showInfoToast(String resourceName, String fallback) { + String text = getString(resourceName, fallback); + Utils.showToastShort(text); + } + + private static void showErrorToast(String resourceName, String fallback, IOException exception) { + String text = getString(resourceName, fallback); + Utils.showToastLong(text + ' ' + exception.getLocalizedMessage()); + } + + private static Response fetch(String url) throws IOException { + Request request = new Request.Builder().url(url).build(); + Response response = client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new IOException("Got HTTP status code " + response.code()); + } + return response; + } + + /** + * {@code inputStream.transferTo(outputStream)} is "too new". + */ + private static void transferTo(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024 * 8]; + int length; + while ((length = in.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + } + + /** + * Gets all file names. + */ + private static Stream lines(Response response) { + BufferedReader reader = new BufferedReader(response.body().charStream()); + return reader.lines().filter(line -> !line.startsWith("#")); + } + + private static String replaceFileName(String uri, String newName) { + return uri.substring(0, uri.lastIndexOf('/') + 1) + newName; + } +} diff --git a/extensions/strava/stub/build.gradle.kts b/extensions/strava/stub/build.gradle.kts new file mode 100644 index 000000000..ffdfac5a6 --- /dev/null +++ b/extensions/strava/stub/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } +} diff --git a/extensions/strava/stub/src/main/AndroidManifest.xml b/extensions/strava/stub/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e7c2ae6 --- /dev/null +++ b/extensions/strava/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java new file mode 100644 index 000000000..ba3ab4ecd --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaContent.java @@ -0,0 +1,15 @@ +package com.strava.core.data; + +import java.io.Serializable; + +public interface MediaContent extends Serializable { + String getCaption(); + + String getId(); + + String getReferenceId(); + + MediaType getType(); + + void setCaption(String caption); +} diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java new file mode 100644 index 000000000..6f4b3d104 --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaDimension.java @@ -0,0 +1,44 @@ +package com.strava.core.data; + +import java.io.Serializable; + +public final class MediaDimension implements Comparable, Serializable { + private final int height; + private final int width; + + public MediaDimension(int width, int height) { + this.width = width; + this.height = height; + } + + public int getHeight() { + return height; + } + + public float getHeightScale() { + if (width <= 0 || height <= 0) { + return 1f; + } + return height / width; + } + + public int getWidth() { + return width; + } + + public float getWidthScale() { + if (width <= 0 || height <= 0) { + return 1f; + } + return width / height; + } + + public boolean isLandscape() { + return width > 0 && width >= height; + } + + @Override + public int compareTo(MediaDimension other) { + return 0; + } +} diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java new file mode 100644 index 000000000..7fb100b5c --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/core/data/MediaType.java @@ -0,0 +1,16 @@ +package com.strava.core.data; + +public enum MediaType { + PHOTO(1), + VIDEO(2); + + private final int remoteValue; + + private MediaType(int remoteValue) { + this.remoteValue = remoteValue; + } + + public int getRemoteValue() { + return remoteValue; + } +} diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java new file mode 100644 index 000000000..4c190ab56 --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaContent.java @@ -0,0 +1,17 @@ +package com.strava.core.data; + +import java.util.SortedMap; + +public interface RemoteMediaContent extends MediaContent { + MediaDimension getLargestSize(); + + String getLargestUrl(); + + SortedMap getSizes(); + + String getSmallestUrl(); + + RemoteMediaStatus getStatus(); + + SortedMap getUrls(); +} diff --git a/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java new file mode 100644 index 000000000..65dda77ed --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/core/data/RemoteMediaStatus.java @@ -0,0 +1,11 @@ +package com.strava.core.data; + +public enum RemoteMediaStatus { + NEW, + PENDING, + PROCESSED, + REPORTED, + REINSTATED, + DELETED, + FAILED +} diff --git a/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java b/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java new file mode 100644 index 000000000..46b4add71 --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/photos/data/Media.java @@ -0,0 +1,286 @@ +package com.strava.photos.data; + +import com.strava.core.data.MediaDimension; +import com.strava.core.data.MediaType; +import com.strava.core.data.RemoteMediaContent; +import com.strava.core.data.RemoteMediaStatus; +import java.util.SortedMap; + +public abstract class Media implements RemoteMediaContent { + public static final class Photo extends Media { + private final Long activityId; + private final String activityName; + private final long athleteId; + private String caption; + private final String createdAt; + private final String createdAtLocal; + private final String cursor; + private final String id; + private final SortedMap sizes; + private final RemoteMediaStatus status; + private final String tag; + private final MediaType type; + private final SortedMap urls; + + @Override + public Long getActivityId() { + return activityId; + } + + @Override + public String getActivityName() { + return activityName; + } + + @Override + public long getAthleteId() { + return athleteId; + } + + @Override + public String getCaption() { + return caption; + } + + @Override + public String getCreatedAt() { + return createdAt; + } + + @Override + public String getCreatedAtLocal() { + return createdAtLocal; + } + + @Override + public String getCursor() { + return cursor; + } + + @Override + public String getId() { + return id; + } + + @Override + public SortedMap getSizes() { + return sizes; + } + + @Override + public RemoteMediaStatus getStatus() { + return status; + } + + @Override + public String getTag() { + return tag; + } + + @Override + public MediaType getType() { + return type; + } + + @Override + public SortedMap getUrls() { + return urls; + } + + @Override + public void setCaption(String caption) { + this.caption = caption; + } + + public Photo(String id, + String caption, + SortedMap urls, + SortedMap sizes, + long athleteId, + String createdAt, + String createdAtLocal, + Long activityId, + String activityName, + RemoteMediaStatus status, + String tag, + String cursor) { + this.id = id; + this.caption = caption; + this.urls = urls; + this.sizes = sizes; + this.athleteId = athleteId; + this.createdAt = createdAt; + this.createdAtLocal = createdAtLocal; + this.activityId = activityId; + this.activityName = activityName; + this.status = status; + this.tag = tag; + this.cursor = cursor; + this.type = MediaType.PHOTO; + } + } + + public static final class Video extends Media { + private final Long activityId; + private final String activityName; + private final long athleteId; + private String caption; + private final String createdAt; + private final String createdAtLocal; + private final String cursor; + private final Float durationSeconds; + private final String id; + private final SortedMap sizes; + private final RemoteMediaStatus status; + private final String tag; + private final MediaType type; + private final SortedMap urls; + private final String videoUrl; + + @Override + public Long getActivityId() { + return activityId; + } + + @Override + public String getActivityName() { + return activityName; + } + + @Override + public long getAthleteId() { + return athleteId; + } + + @Override + public String getCaption() { + return caption; + } + + @Override + public String getCreatedAt() { + return createdAt; + } + + @Override + public String getCreatedAtLocal() { + return createdAtLocal; + } + + @Override + public String getCursor() { + return cursor; + } + + public final Float getDurationSeconds() { + return durationSeconds; + } + + @Override + public String getId() { + return id; + } + + @Override + public SortedMap getSizes() { + return sizes; + } + + @Override + public RemoteMediaStatus getStatus() { + return status; + } + + @Override + public String getTag() { + return tag; + } + + @Override + public MediaType getType() { + return type; + } + + @Override + public SortedMap getUrls() { + return urls; + } + + public final String getVideoUrl() { + return videoUrl; + } + + @Override + public void setCaption(String caption) { + this.caption = caption; + } + + public Video(String id, + String caption, + SortedMap urls, + SortedMap sizes, + long athleteId, + String createdAt, + String createdAtLocal, + Long activityId, + String activityName, + RemoteMediaStatus status, + String videoUrl, + Float durationSeconds, + String tag, + String cursor) { + this.id = id; + this.caption = caption; + this.urls = urls; + this.sizes = sizes; + this.athleteId = athleteId; + this.createdAt = createdAt; + this.createdAtLocal = createdAtLocal; + this.activityId = activityId; + this.activityName = activityName; + this.status = status; + this.videoUrl = videoUrl; + this.durationSeconds = durationSeconds; + this.tag = tag; + this.cursor = cursor; + this.type = MediaType.VIDEO; + } + } + + public abstract Long getActivityId(); + + public abstract String getActivityName(); + + public abstract long getAthleteId(); + + public abstract String getCreatedAt(); + + public abstract String getCreatedAtLocal(); + + public abstract String getCursor(); + + @Override + public MediaDimension getLargestSize() { + return null; + } + + @Override + public String getLargestUrl() { + return null; + } + + @Override + public String getReferenceId() { + return null; + } + + @Override + public String getSmallestUrl() { + return null; + } + + public abstract String getTag(); + + private Media() { + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java new file mode 100644 index 000000000..cf010a151 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch.java @@ -0,0 +1,30 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class PauseOnAudioInterruptPatch { + + private static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = -3; + private static final int AUDIOFOCUS_LOSS_TRANSIENT = -2; + + /** + * Injection point for AudioFocusRequest builder. + * Returns true if audio ducking should be disabled (willPauseWhenDucked = true). + */ + public static boolean shouldPauseOnAudioInterrupt() { + return Settings.PAUSE_ON_AUDIO_INTERRUPT.get(); + } + + /** + * Injection point for onAudioFocusChange callback. + * Converts AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK to AUDIOFOCUS_LOSS_TRANSIENT + * when the setting is enabled, causing YouTube to pause instead of ducking. + */ + public static int overrideAudioFocusChange(int focusChange) { + if (Settings.PAUSE_ON_AUDIO_INTERRUPT.get() && focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + return AUDIOFOCUS_LOSS_TRANSIENT; + } + return focusChange; + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java index 33ac4adc4..4c56466b2 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -247,8 +247,13 @@ public final class LayoutComponentsFilter extends Filter { "sponsorships" ); + final var crowdfundingBox = new StringFilterGroup( + Settings.HIDE_CROWDFUNDING_BOX, + "donation_shelf" + ); + final var channelWatermark = new StringFilterGroup( - Settings.HIDE_VIDEO_CHANNEL_WATERMARK, + Settings.HIDE_CHANNEL_WATERMARK, "featured_channel_watermark_overlay" ); @@ -312,6 +317,7 @@ public final class LayoutComponentsFilter extends Filter { compactChannelBar, compactChannelBarInner, communityPosts, + crowdfundingBox, emergencyBox, expandableMetadata, forYouShelf, @@ -427,7 +433,7 @@ public final class LayoutComponentsFilter extends Filter { * Injection point. */ public static boolean showWatermark() { - return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get(); + return !Settings.HIDE_CHANNEL_WATERMARK.get(); } /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index b85939e7e..21f3184b5 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -96,7 +96,6 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE); public static final BooleanSetting HIDE_COMMUNITY_POSTS = new BooleanSetting("revanced_hide_community_posts", FALSE); public static final BooleanSetting HIDE_COMPACT_BANNER = new BooleanSetting("revanced_hide_compact_banner", TRUE); - public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true); public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message"); public static final BooleanSetting HIDE_EXPANDABLE_CARD = new BooleanSetting("revanced_hide_expandable_card", TRUE); public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true); @@ -158,6 +157,8 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE); public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true); public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE); + public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); + public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true); public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE); public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE); public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true); @@ -172,7 +173,6 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE); public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE); public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE); - public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE); public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE); @@ -356,6 +356,7 @@ public class Settings extends BaseSettings { public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false); public static final BooleanSetting LOOP_VIDEO = new BooleanSetting("revanced_loop_video", FALSE); public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE); + public static final BooleanSetting PAUSE_ON_AUDIO_INTERRUPT = new BooleanSetting("revanced_pause_on_audio_interrupt", FALSE, true); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE); public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE); diff --git a/gradle.properties b/gradle.properties index a04789718..2ae4eb89c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 5.47.0 +version = 5.48.0-dev.13 diff --git a/patches/api/patches.api b/patches/api/patches.api index fdb8c1cf2..abe4c4027 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -104,6 +104,10 @@ public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePa public static final fun setPackageNameOption (Lapp/revanced/patcher/patch/Option;)V } +public final class app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrityKt { + public static final fun getDisablePlayIntegrityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt { public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z @@ -120,6 +124,10 @@ public final class app/revanced/patches/all/misc/screencapture/RemoveScreenCaptu public static final fun getRemoveScreenCaptureRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatchKt { + public static final fun getPreventScreenshotDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatchKt { public static final fun getRemoveScreenshotRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -176,6 +184,10 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatchKt { + public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/cricbuzz/ads/DisableAdsPatchKt { public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -300,6 +312,10 @@ public final class app/revanced/patches/instagram/hide/explore/HideExploreFeedKt public static final fun getHideExploreFeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatchKt { + public static final fun getHideHighlightsTrayPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/instagram/hide/navigation/HideNavigationButtonsKt { public static final fun getHideNavigationButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -324,6 +340,10 @@ public final class app/revanced/patches/instagram/misc/links/OpenLinksExternally public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatchKt { + public static final fun getRemoveBuildExpiredPopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/instagram/misc/share/domain/ChangeLinkSharingDomainPatchKt { public static final fun getChangeLinkSharingDomainPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -348,6 +368,10 @@ public final class app/revanced/patches/letterboxd/ads/HideAdsPatchKt { public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatchKt { + public static final fun getUnlockAppIconsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatchKt { public static final fun getDisableMandatoryLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -645,6 +669,10 @@ public final class app/revanced/patches/protonvpn/delay/RemoveDelayPatchKt { public static final fun getRemoveDelayPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunnelingKt { + public static final fun getUnlockSplitTunnelingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatchKt { public static final fun getHidePurchaseReminderPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -928,6 +956,10 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens public static final fun getDisableLicenseCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/shared/misc/privacy/DisableSentryTelemetryKt { + public static final fun getDisableSentryTelemetryPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt { public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch; @@ -1180,6 +1212,34 @@ public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt { public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } +public final class app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivityKt { + public static final fun getAddGiveGroupKudosButtonToGroupActivity ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/media/download/AddMediaDownloadPatchKt { + public static final fun getAddMediaDownloadPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatchKt { + public static final fun getOverwriteMediaUploadParametersPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/misc/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/password/EnablePasswordLoginPatchKt { + public static final fun getEnablePasswordLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatchKt { + public static final fun getBlockSnowplowTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/quickedit/DisableQuickEditPatchKt { + public static final fun getDisableQuickEditPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt { public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -1660,6 +1720,10 @@ public final class app/revanced/patches/youtube/misc/announcements/Announcements public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatchKt { + public static final fun getPauseOnAudioInterruptPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatchKt { public static final fun getAutoRepeatPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -1955,6 +2019,7 @@ public final class app/revanced/util/BytecodeUtilsKt { public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V + public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V @@ -1964,7 +2029,6 @@ public final class app/revanced/util/BytecodeUtilsKt { public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V - public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index 6153055b9..fa7bd65bd 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -50,12 +50,9 @@ kotlin { publishing { repositories { maven { - name = "GitHubPackages" + name = "githubPackages" url = uri("https://maven.pkg.github.com/revanced/revanced-patches") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } + credentials(PasswordCredentials::class) } } } \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt new file mode 100644 index 000000000..25a948e34 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/playintegrity/DisablePlayIntegrity.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.all.misc.playintegrity + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/playintegrity/DisablePlayIntegrityPatch;" + +private val CONTEXT_BIND_SERVICE_METHOD_REFERENCE = ImmutableMethodReference( + "Landroid/content/Context;", + "bindService", + listOf("Landroid/content/Intent;", "Landroid/content/ServiceConnection;", "I"), + "Z" +) + + +@Suppress("unused") +val disablePlayIntegrityPatch = bytecodePatch( + name = "Disable Play Integrity", + description = "Prevents apps from using Play Integrity by pretending it is not available.", + use = false, +) { + extendWith("extensions/all/misc/disable-play-integrity.rve") + + dependsOn( + transformInstructionsPatch( + filterMap = filterMap@{ classDef, method, instruction, instructionIndex -> + val reference = instruction + .getReference() + ?.takeIf { + MethodUtil.methodSignaturesMatch(CONTEXT_BIND_SERVICE_METHOD_REFERENCE, it) + } + ?: return@filterMap null + + Triple(instruction as Instruction35c, instructionIndex, reference.parameterTypes) + }, + transform = { method, entry -> + val (instruction, index, parameterTypes) = entry + val parameterString = parameterTypes.joinToString(separator = "") + val registerString = "v${instruction.registerC}, v${instruction.registerD}, v${instruction.registerE}, v${instruction.registerF}" + + method.replaceInstruction( + index, + "invoke-static { $registerString }, $EXTENSION_CLASS_DESCRIPTOR->bindService(Landroid/content/Context;$parameterString)Z" + ) + } + ) + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt new file mode 100644 index 000000000..46e9eefa8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/PreventScreenshotDetectionPatch.kt @@ -0,0 +1,51 @@ +package app.revanced.patches.all.misc.screenshot + +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private val registerScreenCaptureCallbackMethodReference = ImmutableMethodReference( + "Landroid/app/Activity;", + "registerScreenCaptureCallback", + listOf( + "Ljava/util/concurrent/Executor;", + "Landroid/app/Activity\$ScreenCaptureCallback;", + ), + "V" +) + +private val unregisterScreenCaptureCallbackMethodReference = ImmutableMethodReference( + "Landroid/app/Activity;", + "unregisterScreenCaptureCallback", + listOf( + "Landroid/app/Activity\$ScreenCaptureCallback;", + ), + "V" +) + +@Suppress("unused") +val preventScreenshotDetectionPatch = bytecodePatch( + name = "Prevent screenshot detection", + description = "Removes the registration of all screen capture callbacks. This prevents the app from detecting screenshots.", +) { + dependsOn(transformInstructionsPatch( + filterMap = { _, _, instruction, instructionIndex -> + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@transformInstructionsPatch null + + val reference = instruction.getReference() ?: return@transformInstructionsPatch null + + instructionIndex.takeIf { + MethodUtil.methodSignaturesMatch(reference, registerScreenCaptureCallbackMethodReference) || + MethodUtil.methodSignaturesMatch(reference, unregisterScreenCaptureCallbackMethodReference) + } + }, + transform = { mutableMethod, instructionIndex -> + mutableMethod.removeInstruction(instructionIndex) + } + )) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt new file mode 100644 index 000000000..bf2ee4120 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.com.sbs.ondemand.tv + +import app.revanced.patcher.fingerprint + +internal val shouldShowAdvertisingTVFingerprint = fingerprint { + returns("Z") + custom { method, classDef -> + method.name == "getShouldShowAdvertisingTV" && + classDef.type == "Lcom/sbs/ondemand/common/InMemoryStorage;" + } +} + +internal val shouldShowPauseAdFingerprint = fingerprint { + returns("Z") + custom { method, classDef -> + method.name == "shouldShowPauseAd" && + classDef.type == "Lcom/sbs/ondemand/player/viewmodels/PauseAdController;" + } +} + +internal val requestAdStreamFingerprint = fingerprint { + returns("V") + custom { method, classDef -> + method.name == "requestAdStream\$player_googleStoreTvRelease" && + classDef.type == "Lcom/sbs/ondemand/player/viewmodels/AdsController;" + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt new file mode 100644 index 000000000..1b628562a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/com/sbs/ondemand/tv/RemoveAdsPatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.com.sbs.ondemand.tv + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.misc.pairip.license.disableLicenseCheckPatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val removeAdsPatch = bytecodePatch( + name = "Remove ads", + description = "Removes pre-roll, pause and on-demand advertisements from SBS On Demand TV.", +) { + compatibleWith("com.sbs.ondemand.tv") + + dependsOn(disableLicenseCheckPatch) + + execute { + shouldShowAdvertisingTVFingerprint.method.returnEarly(true) + shouldShowPauseAdFingerprint.method.returnEarly(false) + + // Remove on-demand pre-roll advertisements using exception handling. + // Exception handling is used instead of returnEarly() because: + // 1. returnEarly() causes black screen when the app waits for ad content that never comes. + // 2. SBS app has built-in exception handling in handleProviderFailure(). + // 3. Exception triggers fallbackToAkamaiProvider() which loads actual content. + // 4. This preserves the intended app flow: first try ads, then fail gracefully, then load content. + requestAdStreamFingerprint.method.addInstructions( + 0, + """ + new-instance v0, Ljava/lang/RuntimeException; + const-string v1, "Ad stream disabled" + invoke-direct {v0, v1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V + throw v0 + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt index e93cafc0d..5b0f551cd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/disneyplus/SkipAdsPatch.kt @@ -8,11 +8,7 @@ val skipAdsPatch = bytecodePatch( name = "Skip ads", description = "Automatically skips ads.", ) { - compatibleWith( - "com.disney.disneyplus", - "in.startv.hotstar", - "in.startv.hotstaronly", - ) + compatibleWith("com.disney.disneyplus") execute { arrayOf(insertionGetPointsFingerprint, insertionGetRangesFingerprint).forEach { diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt index a2c7d5ba5..f9ec6505f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/explore/HideExploreFeed.kt @@ -1,29 +1,7 @@ package app.revanced.patches.instagram.hide.explore -import app.revanced.patcher.Fingerprint -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction - -context(BytecodePatchContext) -internal fun Fingerprint.replaceJsonFieldWithBogus( - key: String, -) { - val targetStringIndex = stringMatches!!.first { match -> match.string == key }.index - val targetStringRegister = method.getInstruction(targetStringIndex).registerA - - /** - * Replacing the JSON key we want to skip with a random string that is not a valid JSON key. - * This way the feeds array will never be populated. - * Received JSON keys that are not handled are simply ignored, so there are no side effects. - */ - method.replaceInstruction( - targetStringIndex, - "const-string v$targetStringRegister, \"BOGUS\"", - ) -} +import app.revanced.patches.instagram.shared.replaceStringWithBogus @Suppress("unused") val hideExploreFeedPatch = bytecodePatch( @@ -34,6 +12,6 @@ val hideExploreFeedPatch = bytecodePatch( compatibleWith("com.instagram.android") execute { - exploreResponseJsonParserFingerprint.replaceJsonFieldWithBogus(EXPLORE_KEY_TO_BE_HIDDEN) + exploreResponseJsonParserFingerprint.replaceStringWithBogus(EXPLORE_KEY_TO_BE_HIDDEN) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt new file mode 100644 index 000000000..9a0341ee6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.instagram.hide.highlightsTray + +import app.revanced.patcher.fingerprint + +internal const val TARGET_STRING = "highlights_tray" + +internal val highlightsUrlBuilderFingerprint = fingerprint { + strings(TARGET_STRING,"X-IG-Accept-Hint") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt new file mode 100644 index 000000000..3b777d9ed --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/highlightsTray/HideHighlightsTrayPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.instagram.hide.highlightsTray + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.instagram.shared.replaceStringWithBogus + +@Suppress("unused") +val hideHighlightsTrayPatch = bytecodePatch( + name = "Hide highlights tray", + description = "Hides the highlights tray in profile section.", + use = false +) { + compatibleWith("com.instagram.android") + + execute { + highlightsUrlBuilderFingerprint.replaceStringWithBogus(TARGET_STRING) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt index fca74bc18..b946abf6b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/navigation/HideNavigationButtons.kt @@ -28,6 +28,13 @@ val hideNavigationButtonsPatch = bytecodePatch( dependsOn(sharedExtensionPatch) + val hideHome by booleanOption( + key = "hideHome", + default = false, + title = "Hide Home", + description = "Permanently hides the Home button. App starts at next available tab." // On the "homecoming" / current instagram layout. + ) + val hideReels by booleanOption( key = "hideReels", default = true, @@ -35,6 +42,27 @@ val hideNavigationButtonsPatch = bytecodePatch( description = "Permanently hides the Reels button." ) + val hideDirect by booleanOption( + key = "hideDirect", + default = false, + title = "Hide Direct", + description = "Permanently hides the Direct button." + ) + + val hideSearch by booleanOption( + key = "hideSearch", + default = false, + title = "Hide Search", + description = "Permanently hides the Search button." + ) + + val hideProfile by booleanOption( + key = "hideProfile", + default = false, + title = "Hide Profile", + description = "Permanently hides the Profile button." + ) + val hideCreate by booleanOption( key = "hideCreate", default = true, @@ -43,7 +71,7 @@ val hideNavigationButtonsPatch = bytecodePatch( ) execute { - if (!hideReels!! && !hideCreate!!) { + if (!hideHome!! &&!hideReels!! && !hideDirect!! && !hideSearch!! && !hideProfile!! && !hideCreate!!) { return@execute Logger.getLogger(this::class.java.name).warning( "No hide navigation buttons options are enabled. No changes made." ) @@ -76,6 +104,13 @@ val hideNavigationButtonsPatch = bytecodePatch( """ } + if (hideHome!!) { + addInstructionsAtControlFlowLabel( + returnIndex, + instructionsRemoveButtonByName("fragment_feed") + ) + } + if (hideReels!!) { addInstructionsAtControlFlowLabel( returnIndex, @@ -83,12 +118,33 @@ val hideNavigationButtonsPatch = bytecodePatch( ) } + if (hideDirect!!) { + addInstructionsAtControlFlowLabel( + returnIndex, + instructionsRemoveButtonByName("fragment_direct_tab") + ) + } + if (hideSearch!!) { + addInstructionsAtControlFlowLabel( + returnIndex, + instructionsRemoveButtonByName("fragment_search") + ) + } + if (hideCreate!!) { addInstructionsAtControlFlowLabel( returnIndex, instructionsRemoveButtonByName("fragment_share") ) } + + if (hideProfile!!) { + addInstructionsAtControlFlowLabel( + returnIndex, + instructionsRemoveButtonByName("fragment_profile") + ) + } + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt index 0c2501411..9dc6dbf59 100644 --- a/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/hide/suggestions/HideSuggestedContent.kt @@ -1,7 +1,7 @@ package app.revanced.patches.instagram.hide.suggestions import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.instagram.hide.explore.replaceJsonFieldWithBogus +import app.revanced.patches.instagram.shared.replaceStringWithBogus @Suppress("unused") val hideSuggestedContent = bytecodePatch( @@ -13,7 +13,7 @@ val hideSuggestedContent = bytecodePatch( execute { FEED_ITEM_KEYS_TO_BE_HIDDEN.forEach { key -> - feedItemParseFromJsonFingerprint.replaceJsonFieldWithBogus(key) + feedItemParseFromJsonFingerprint.replaceStringWithBogus(key) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt new file mode 100644 index 000000000..ad4a66540 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/Fingerprints.kt @@ -0,0 +1,12 @@ + +package app.revanced.patches.instagram.misc.removeBuildExpiredPopup + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal + +internal const val MILLISECOND_IN_A_DAY_LITERAL = 0x5265c00L + +internal val appUpdateLockoutBuilderFingerprint = fingerprint { + strings("android.hardware.sensor.hinge_angle") + literal { MILLISECOND_IN_A_DAY_LITERAL } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt new file mode 100644 index 000000000..9d19e928e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/removeBuildExpiredPopup/RemoveBuildExpiredPopupPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.instagram.misc.removeBuildExpiredPopup + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +@Suppress("unused") +val removeBuildExpiredPopupPatch = bytecodePatch( + name = "Remove build expired popup", + description = "Removes the popup that appears after a while, when the app version ages.", +) { + compatibleWith("com.instagram.android") + + execute { + appUpdateLockoutBuilderFingerprint.method.apply { + val longToIntIndex = instructions.first { it.opcode == Opcode.LONG_TO_INT }.location.index + val appAgeRegister = getInstruction(longToIntIndex).registerA + + // Set app age to 0 days old such that the build expired popup doesn't appear. + addInstruction(longToIntIndex + 1, "const v$appAgeRegister, 0x0") + } + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt index 9e0d8e64d..9f2d9fc4e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/misc/share/PermalinkResponseJsonParserFingerprint.kt @@ -13,7 +13,7 @@ internal val storyUrlResponseJsonParserFingerprint = fingerprint { } internal val profileUrlResponseJsonParserFingerprint = fingerprint { - strings("profile_to_share_url", "ProfileUrlResponse") + strings("profile_to_share_url") custom { method, _ -> method.name == "parseFromJson" } } diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt index acb8235fb..059a30065 100644 --- a/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/reels/DisableReelsScrollingPatch.kt @@ -9,7 +9,7 @@ val disableReelsScrollingPatch = bytecodePatch( name = "Disable Reels scrolling", description = "Disables the endless scrolling behavior in Instagram Reels, preventing swiping to the next Reel. " + "Note: On a clean install, the 'Tip' animation may appear but will stop on its own after a few seconds.", - use = true + use = false ) { compatibleWith("com.instagram.android") @@ -31,4 +31,4 @@ val disableReelsScrollingPatch = bytecodePatch( // Return false in onInterceptTouchEvent to disable pull-to-refresh. clipsSwipeRefreshLayoutOnInterceptTouchEventFingerprint.method.returnEarly(false) } -} \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt new file mode 100644 index 000000000..522257aa8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/shared/Utils.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.instagram.shared + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +context(BytecodePatchContext) +internal fun Fingerprint.replaceStringWithBogus( + targetString: String, +) { + val targetStringIndex = stringMatches!!.first { match -> match.string == targetString }.index + val targetStringRegister = method.getInstruction(targetStringIndex).registerA + + /** + * Replaces the 'target string' with 'BOGUS'. + * This is usually done when we need to override a JSON key or url, + * to skip with a random string that is not a valid JSON key. + */ + method.replaceInstruction( + targetStringIndex, + "const-string v$targetStringRegister, \"BOGUS\"", + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt new file mode 100644 index 000000000..1b549cd57 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.letterboxd.unlock.unlockAppIcons + +import app.revanced.patcher.fingerprint + +internal val getCanChangeAppIconFingerprint = fingerprint { + custom { method, classDef -> + method.name == "getCanChangeAppIcon" && classDef.type.endsWith("SettingsAppIconFragment;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt new file mode 100644 index 000000000..54d6f3df9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/letterboxd/unlock/unlockAppIcons/UnlockAppIconsPatch.kt @@ -0,0 +1,16 @@ + +package app.revanced.patches.letterboxd.unlock.unlockAppIcons + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val unlockAppIconsPatch = bytecodePatch( + name = "Unlock app icons", +) { + compatibleWith("com.letterboxd.letterboxd") + + execute { + getCanChangeAppIconFingerprint.method.returnEarly(true) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt new file mode 100644 index 000000000..639141723 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.protonvpn.splittunneling + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val enableSplitTunnelingUiFingerprint = fingerprint { + strings("currentModeAppNames") + opcodes( + Opcode.MOVE_OBJECT, + Opcode.MOVE_FROM16, + Opcode.INVOKE_DIRECT_RANGE + ) +} + +internal val initializeSplitTunnelingSettingsUIFingerprint = fingerprint { + custom { method, _ -> + method.name == "applyRestrictions" + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt new file mode 100644 index 000000000..f50a5f993 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/protonvpn/splittunneling/UnlockSplitTunneling.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.protonvpn.splittunneling + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val unlockSplitTunnelingPatch = + bytecodePatch( + name = "Unlock split tunneling", + ) { + compatibleWith("ch.protonvpn.android") + + execute { + val registerIndex = enableSplitTunnelingUiFingerprint.patternMatch!!.endIndex - 1 + + enableSplitTunnelingUiFingerprint.method.apply { + val register = getInstruction(registerIndex).registerA + replaceInstruction(registerIndex, "const/4 v$register, 0x0") + } + + initializeSplitTunnelingSettingsUIFingerprint.method.apply { + val initSettingsIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "getSplitTunneling" + } + removeInstruction(initSettingsIndex - 1) + } + } + } diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt index 8cb3f5518..40c23a76c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt @@ -14,8 +14,8 @@ val fixAudioMissingInDownloadsPatch = bytecodePatch( execute { val endpointReplacements = mapOf( - "/DASH_audio.mp4" to "/DASH_AUDIO_128.mp4", - "/audio" to "/DASH_AUDIO_64.mp4", + "/DASH_audio.mp4" to "/CMAF_AUDIO_128.mp4", + "/audio" to "/CMAF_AUDIO_64.mp4", ) downloadAudioFingerprint.method.apply { diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt new file mode 100644 index 000000000..f361a26bb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/privacy/DisableSentryTelemetry.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.shared.misc.privacy + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.asSequence +import app.revanced.util.getNode +import org.w3c.dom.Element + +@Suppress("unused") +val disableSentryTelemetryPatch = resourcePatch( + name = "Disable Sentry telemetry", + description = "Disables Sentry telemetry. See https://sentry.io/for/android/ for more information.", + use = false, +) { + execute { + fun Element.replaceOrCreate(tagName: String, attributeName: String, attributeValue: String) { + val childElements = getElementsByTagName(tagName).asSequence().filterIsInstance() + val targetChild = childElements.find { childElement -> + childElement.getAttribute("android:name") == attributeName + } + if (targetChild != null) { + targetChild.setAttribute("android:value", attributeValue) + } else { + appendChild(ownerDocument.createElement(tagName).apply { + setAttribute("android:name", attributeName) + setAttribute("android:value", attributeValue) + }) + } + } + + document("AndroidManifest.xml").use { document -> + val application = document.getNode("application") as Element + application.replaceOrCreate("meta-data", "io.sentry.enabled", "false") + application.replaceOrCreate("meta-data", "io.sentry.dsn", "") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt new file mode 100644 index 000000000..adc2b69ba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/AddGiveGroupKudosButtonToGroupActivity.kt @@ -0,0 +1,201 @@ +package app.revanced.patches.strava.groupkudos + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.util.childElementsSequence +import app.revanced.util.findElementByAttributeValueOrThrow +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.AccessFlags.* +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31i +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef +import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import org.w3c.dom.Element + +private const val VIEW_CLASS_DESCRIPTOR = "Landroid/view/View;" +private const val ON_CLICK_LISTENER_CLASS_DESCRIPTOR = "Landroid/view/View\$OnClickListener;" + +private var shakeToKudosStringId = -1 +private var kudosIdId = -1 +private var leaveIdId = -1 + +private val addGiveKudosButtonToLayoutPatch = resourcePatch { + fun String.toResourceId() = substring(2).toInt(16) + + execute { + document("res/values/public.xml").use { public -> + fun Sequence.firstByName(name: String) = first { + it.getAttribute("name") == name + } + + val publicElements = public.documentElement.childElementsSequence().filter { + it.tagName == "public" + } + val idElements = publicElements.filter { + it.getAttribute("type") == "id" + } + val stringElements = publicElements.filter { + it.getAttribute("type") == "string" + } + + shakeToKudosStringId = + stringElements.firstByName("shake_to_kudos_dialog_title").getAttribute("id").toResourceId() + + val kudosIdNode = idElements.firstByName("kudos").apply { + kudosIdId = getAttribute("id").toResourceId() + } + + document("res/layout/grouped_activities_dialog_group_tab.xml").use { layout -> + layout.childNodes.findElementByAttributeValueOrThrow("android:id", "@id/leave_group_button_container") + .apply { + // Change from "FrameLayout". + layout.renameNode(this, namespaceURI, "LinearLayout") + + val leaveButton = childElementsSequence().first() + // Get "Leave Group" button ID for bytecode matching. + val leaveButtonIdName = leaveButton.getAttribute("android:id").substringAfter('/') + leaveIdId = idElements.firstByName(leaveButtonIdName).getAttribute("id").toResourceId() + + // Add surrounding padding to offset decrease on buttons. + setAttribute("android:paddingHorizontal", "@dimen/space_2xs") + + // Place buttons next to each other with equal width. + val kudosButton = leaveButton.apply { + setAttribute("android:layout_width", "0dp") + setAttribute("android:layout_weight", "1") + // Decrease padding between buttons from "@dimen/button_large_padding" ... + setAttribute("android:paddingHorizontal", "@dimen/space_xs") + }.cloneNode(true) as Element + kudosButton.apply { + setAttribute("android:id", "@id/${kudosIdNode.getAttribute("name")}") + setAttribute("android:text", "@string/kudos_button") + }.let(::appendChild) + + // Downgrade emphasis of "Leave Group" button from "primary". + leaveButton.setAttribute("app:emphasis", "secondary") + } + } + } + } +} + +@Suppress("unused") +val addGiveGroupKudosButtonToGroupActivity = bytecodePatch( + name = "Add 'Give Kudos' button to 'Group Activity'", + description = "Adds a button that triggers the same action as shaking your phone would." +) { + compatibleWith("com.strava") + + dependsOn(addGiveKudosButtonToLayoutPatch) + + execute { + val className = initFingerprint.originalClassDef.type + val onClickListenerClassName = "${className.substringBeforeLast(';')}\$GiveKudosOnClickListener;" + + initFingerprint.method.apply { + val constLeaveIdInstruction = instructions.filterIsInstance().first { + it.narrowLiteral == leaveIdId + } + val findViewByIdInstruction = + getInstruction(constLeaveIdInstruction.location.index + 1) + val moveViewInstruction = getInstruction(constLeaveIdInstruction.location.index + 2) + val checkCastButtonInstruction = + getInstruction(constLeaveIdInstruction.location.index + 3) + + val buttonClassName = checkCastButtonInstruction.getReference()!!.type + + addInstructions( + constLeaveIdInstruction.location.index, + """ + ${constLeaveIdInstruction.opcode.name} v${constLeaveIdInstruction.registerA}, $kudosIdId + ${findViewByIdInstruction.opcode.name} { v${findViewByIdInstruction.registerC}, v${findViewByIdInstruction.registerD} }, ${findViewByIdInstruction.reference} + ${moveViewInstruction.opcode.name} v${moveViewInstruction.registerA} + ${checkCastButtonInstruction.opcode.name} v${checkCastButtonInstruction.registerA}, ${checkCastButtonInstruction.reference} + new-instance v0, $onClickListenerClassName + invoke-direct { v0, p0 }, $onClickListenerClassName->($className)V + invoke-virtual { p3, v0 }, $buttonClassName->setOnClickListener($ON_CLICK_LISTENER_CLASS_DESCRIPTOR)V + """ + ) + } + + val actionHandlerMethod = actionHandlerFingerprint.match(initFingerprint.originalClassDef).method + val constShakeToKudosStringIndex = actionHandlerMethod.instructions.indexOfFirst { + it is NarrowLiteralInstruction && it.narrowLiteral == shakeToKudosStringId + } + val getSingletonInstruction = actionHandlerMethod.instructions.filterIsInstance().last { + it.opcode == Opcode.SGET_OBJECT && it.location.index < constShakeToKudosStringIndex + } + + val outerThisField = ImmutableField( + onClickListenerClassName, + "outerThis", + className, + PUBLIC.value or FINAL.value or SYNTHETIC.value, + null, + listOf(), + setOf() + ) + + val initFieldMethod = ImmutableMethod( + onClickListenerClassName, + "", + listOf(ImmutableMethodParameter(className, setOf(), "outerThis")), + "V", + PUBLIC.value or SYNTHETIC.value or CONSTRUCTOR.value, + setOf(), + setOf(), + MutableMethodImplementation(2) + ).toMutable().apply { + addInstructions( + """ + invoke-direct {p0}, Ljava/lang/Object;->()V + iput-object p1, p0, $outerThisField + return-void + """ + ) + } + + val onClickMethod = ImmutableMethod( + onClickListenerClassName, + "onClick", + listOf(ImmutableMethodParameter(VIEW_CLASS_DESCRIPTOR, setOf(), "v")), + "V", + PUBLIC.value or FINAL.value, + setOf(), + setOf(), + MutableMethodImplementation(2) + ).toMutable().apply { + addInstructions( + """ + sget-object p1, ${getSingletonInstruction.reference} + iget-object p0, p0, $outerThisField + invoke-virtual { p0, p1 }, ${actionHandlerFingerprint.method} + return-void + """ + ) + } + + ImmutableClassDef( + onClickListenerClassName, + PUBLIC.value or FINAL.value or SYNTHETIC.value, + "Ljava/lang/Object;", + listOf(ON_CLICK_LISTENER_CLASS_DESCRIPTOR), + "ProGuard", // Same as source file name of other classes. + listOf(), + setOf(outerThisField), + setOf(initFieldMethod, onClickMethod) + ).let(classes::add) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt new file mode 100644 index 000000000..f5ce604fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/groupkudos/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.strava.groupkudos + +import app.revanced.patcher.fingerprint + +internal val initFingerprint = fingerprint { + parameters("Lcom/strava/feed/view/modal/GroupTabFragment;" , "Z" , "Landroidx/fragment/app/FragmentManager;") + custom { method, _ -> + method.name == "" + } +} + +internal val actionHandlerFingerprint = fingerprint { + strings("state") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt new file mode 100644 index 000000000..01da305c9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/AddMediaDownloadPatch.kt @@ -0,0 +1,116 @@ +package app.revanced.patches.strava.media.download + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.strava.misc.extension.sharedExtensionPatch +import app.revanced.util.getReference +import app.revanced.util.writeRegister +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +private const val ACTION_CLASS_DESCRIPTOR = "Lcom/strava/bottomsheet/Action;" +private const val MEDIA_CLASS_DESCRIPTOR = "Lcom/strava/photos/data/Media;" +private const val MEDIA_DOWNLOAD_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/AddMediaDownloadPatch;" + +@Suppress("unused") +val addMediaDownloadPatch = bytecodePatch( + name = "Add media download", + description = "Extends the full-screen media viewer menu with items to copy or open their URLs or download them directly." +) { + compatibleWith("com.strava") + + dependsOn( + resourceMappingPatch, + sharedExtensionPatch + ) + + execute { + val fragmentClass = classBy { it.endsWith("/FullscreenMediaFragment;") }!!.mutableClass + + // region Extend menu of `FullscreenMediaFragment` with actions. + + createAndShowFragmentFingerprint.match(fragmentClass).method.apply { + val setTrueIndex = instructions.indexOfFirst { instruction -> + instruction.opcode == Opcode.IPUT_BOOLEAN + } + val actionRegistrarRegister = getInstruction(setTrueIndex).registerB + val actionRegister = instructions.first { instruction -> + instruction.getReference()?.type == ACTION_CLASS_DESCRIPTOR + }.writeRegister!! + + fun addMenuItem(actionId: String, string: String, color: String, drawable: String) = addInstructions( + setTrueIndex + 1, + """ + new-instance v$actionRegister, $ACTION_CLASS_DESCRIPTOR + sget v${actionRegister + 1}, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->$actionId:I + const v${actionRegister + 2}, 0x0 + const v${actionRegister + 3}, ${resourceMappings["string", string]} + const v${actionRegister + 4}, ${resourceMappings["color", color]} + const v${actionRegister + 5}, ${resourceMappings["drawable", drawable]} + move/from16 v${actionRegister + 6}, v${actionRegister + 4} + invoke-direct/range { v$actionRegister .. v${actionRegister + 7} }, $ACTION_CLASS_DESCRIPTOR->(ILjava/lang/String;IIIILjava/io/Serializable;)V + invoke-virtual { v$actionRegistrarRegister, v$actionRegister }, Lcom/strava/bottomsheet/a;->a(Lcom/strava/bottomsheet/BottomSheetItem;)V + """ + ) + + addMenuItem("ACTION_COPY_LINK", "copy_link", "core_o3", "actions_link_normal_xsmall") + addMenuItem("ACTION_OPEN_LINK", "fallback_menu_item_open_in_browser", "core_o3", "actions_link_external_normal_xsmall") + addMenuItem("ACTION_DOWNLOAD", "download", "core_o3", "actions_download_normal_xsmall") + + // Move media to last parameter of `Action` constructor. + val getMediaInstruction = instructions.first { instruction -> + instruction.getReference()?.type == MEDIA_CLASS_DESCRIPTOR + } + addInstruction( + getMediaInstruction.location.index + 1, + "move-object/from16 v${actionRegister + 7}, v${getMediaInstruction.writeRegister}" + ) + } + + // endregion + + // region Handle new actions. + + val actionClass = classes.first { clazz -> + clazz.type == ACTION_CLASS_DESCRIPTOR + } + val actionSerializableField = actionClass.instanceFields.first { field -> + field.type == "Ljava/io/Serializable;" + } + + // Handle "copy link" & "open link" & "download" actions. + handleMediaActionFingerprint.match(fragmentClass).method.apply { + // Call handler if action ID < 0 (= custom). + val moveInstruction = instructions.first { instruction -> + instruction.opcode == Opcode.MOVE_RESULT + } + val indexAfterMoveInstruction = moveInstruction.location.index + 1 + val actionIdRegister = moveInstruction.writeRegister + addInstructionsWithLabels( + indexAfterMoveInstruction, + """ + if-gez v$actionIdRegister, :move + check-cast p2, $ACTION_CLASS_DESCRIPTOR + iget-object v0, p2, $actionSerializableField + check-cast v0, $MEDIA_CLASS_DESCRIPTOR + invoke-static { v$actionIdRegister, v0 }, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->handleAction(I$MEDIA_CLASS_DESCRIPTOR)Z + move-result v0 + return v0 + """, + ExternalLabel("move", instructions[indexAfterMoveInstruction]) + ) + } + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt new file mode 100644 index 000000000..2f6566a82 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/download/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.strava.media.download + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val createAndShowFragmentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + strings("mediaType") +} + +internal val handleMediaActionFingerprint = fingerprint { + parameters("Landroid/view/View;", "Lcom/strava/bottomsheet/BottomSheetItem;") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt new file mode 100644 index 000000000..9653ff76b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.strava.media.upload + +import app.revanced.patcher.fingerprint + +internal val getCompressionQualityFingerprint = fingerprint { + custom { method, _ -> + method.name == "getCompressionQuality" + } +} + +internal val getMaxDurationFingerprint = fingerprint { + custom { method, _ -> + method.name == "getMaxDuration" + } +} + +internal val getMaxSizeFingerprint = fingerprint { + custom { method, _ -> + method.name == "getMaxSize" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt new file mode 100644 index 000000000..1ae9bc18c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/media/upload/OverwriteMediaUploadParametersPatch.kt @@ -0,0 +1,48 @@ +package app.revanced.patches.strava.media.upload + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.intOption +import app.revanced.patcher.patch.longOption +import app.revanced.util.returnEarly + +@Suppress("unused") +val overwriteMediaUploadParametersPatch = bytecodePatch( + name = "Overwrite media upload parameters", + description = "Overwrites the compression, resize and trim media (images and videos) parameters returned by Strava's server before upload.", +) { + compatibleWith("com.strava") + + val compressionQuality by intOption( + key = "compressionQuality", + title = "Compression quality (percent)", + description = "This is used as the JPEG quality setting (≤ 100).", + ) { it == null || it in 1..100 } + + val maxDuration by longOption( + key = "maxDuration", + title = "Max duration (seconds)", + description = "The maximum length (≤ ${60 * 60}) of a video before it gets trimmed.", + ) { it == null || it in 1..60 * 60 } + + val maxSize by intOption( + key = "maxSize", + title = "Max size (pixels)", + description = "The image gets resized so that the smaller dimension (width/height) does not exceed this value (≤ 10000).", + ) { it == null || it in 1..10000 } + + execute { + val mediaUploadParametersClass = classes.single { it.endsWith("/MediaUploadParameters;") } + + compressionQuality?.let { compressionQuality -> + getCompressionQualityFingerprint.match(mediaUploadParametersClass).method.returnEarly(compressionQuality / 100f) + } + + maxDuration?.let { maxDuration -> + getMaxDurationFingerprint.match(mediaUploadParametersClass).method.returnEarly(maxDuration) + } + + maxSize?.let { + getMaxSizeFingerprint.match(mediaUploadParametersClass).method.returnEarly(it) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt new file mode 100644 index 000000000..6c2a97769 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/Hooks.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.strava.misc.extension + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val applicationOnCreateHook = extensionHook { + custom { method, classDef -> + method.name == "onCreate" && classDef.endsWith("/StravaApplication;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..404bf6e9a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.strava.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch("strava", applicationOnCreateHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt new file mode 100644 index 000000000..6b7e74235 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/EnablePasswordLoginPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.strava.password + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val enablePasswordLoginPatch = bytecodePatch( + name = "Enable password login", + description = "Re-enables password login after having used an OTP code.", +) { + compatibleWith("com.strava") + + execute { + fun Fingerprint.returnTrue() = method.returnEarly(true) + + logInGetUsePasswordFingerprint.returnTrue() + emailChangeGetUsePasswordFingerprint.returnTrue() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt new file mode 100644 index 000000000..94c88490a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/password/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.strava.password + +import app.revanced.patcher.fingerprint + +internal val logInGetUsePasswordFingerprint = fingerprint { + custom { method, classDef -> + method.name == "getUsePassword" && classDef.endsWith("/RequestOtpLogInNetworkResponse;") + } +} + +internal val emailChangeGetUsePasswordFingerprint = fingerprint { + custom { method, classDef -> + method.name == "getUsePassword" && classDef.endsWith("/RequestEmailChangeWithOtpOrPasswordResponse;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt new file mode 100644 index 000000000..313a98d1d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/BlockSnowplowTrackingPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.strava.privacy + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val blockSnowplowTrackingPatch = bytecodePatch( + name = "Block Snowplow tracking", + description = "Blocks Snowplow analytics. See https://snowplow.io for more information.", +) { + compatibleWith("com.strava") + + execute { + // Keep events list empty, otherwise sent to https://c.strava.com/com.snowplowanalytics.snowplow/tp2. + insertEventFingerprint.method.returnEarly() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt new file mode 100644 index 000000000..196602ba0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/privacy/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.strava.privacy + +import app.revanced.patcher.fingerprint + +// https://github.com/snowplow/snowplow-android-tracker/blob/2.2.0/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/internal/emitter/storage/SQLiteEventStore.java#L130 +// Not the exact same code (e.g. returns void instead of long), even though the version number matches. +internal val insertEventFingerprint = fingerprint { + strings("Added event to database: %s") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt new file mode 100644 index 000000000..128f86870 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/DisableQuickEditPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.strava.quickedit + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val disableQuickEditPatch = bytecodePatch( + name = "Disable Quick Edit", + description = "Prevents the Quick Edit prompt from popping up.", +) { + compatibleWith("com.strava") + + execute { + getHasAccessToQuickEditFingerprint.method.returnEarly() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt new file mode 100644 index 000000000..acd48542b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/quickedit/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.strava.quickedit + +import app.revanced.patcher.fingerprint + +internal val getHasAccessToQuickEditFingerprint = fingerprint { + returns("Z") + custom { method, _ -> + method.name == "getHasAccessToQuickEdit" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt index 0458f45d3..45583ce4e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt @@ -1,11 +1,9 @@ package app.revanced.patches.strava.subscription import app.revanced.patcher.fingerprint -import com.android.tools.smali.dexlib2.Opcode internal val getSubscribedFingerprint = fingerprint { - opcodes(Opcode.IGET_BOOLEAN) custom { method, classDef -> - classDef.endsWith("/SubscriptionDetailResponse;") && method.name == "getSubscribed" + method.name == "getSubscribed" && classDef.endsWith("/SubscriptionDetailResponse;") } } diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt index e59660472..e9fbc49a4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt @@ -1,7 +1,7 @@ package app.revanced.patches.strava.subscription -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly @Suppress("unused") val unlockSubscriptionPatch = bytecodePatch( @@ -11,9 +11,6 @@ val unlockSubscriptionPatch = bytecodePatch( compatibleWith("com.strava") execute { - getSubscribedFingerprint.method.replaceInstruction( - getSubscribedFingerprint.patternMatch!!.startIndex, - "const/4 v0, 0x1", - ) + getSubscribedFingerprint.method.returnEarly(true) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index ae62a4914..07eecb83f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -174,6 +174,7 @@ val hideLayoutComponentsPatch = bytecodePatch( ), SwitchPreference("revanced_hide_channel_bar"), SwitchPreference("revanced_hide_channel_watermark"), + SwitchPreference("revanced_hide_crowdfunding_box"), SwitchPreference("revanced_hide_emergency_box"), SwitchPreference("revanced_hide_info_panels"), SwitchPreference("revanced_hide_join_membership_button"), @@ -229,7 +230,6 @@ val hideLayoutComponentsPatch = bytecodePatch( SwitchPreference("revanced_hide_chips_shelf"), SwitchPreference("revanced_hide_community_posts"), SwitchPreference("revanced_hide_compact_banner"), - SwitchPreference("revanced_hide_crowdfunding_box"), SwitchPreference("revanced_hide_expandable_card"), SwitchPreference("revanced_hide_floating_microphone_button"), SwitchPreference( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt new file mode 100644 index 000000000..5fb44f20f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.youtube.misc.audiofocus + +import app.revanced.patcher.fingerprint + +internal val audioFocusChangeListenerFingerprint = fingerprint { + strings( + "AudioFocus DUCK", + "AudioFocus loss; Will lower volume", + ) +} + +internal val audioFocusRequestBuilderFingerprint = fingerprint { + strings("Can't build an AudioFocusRequestCompat instance without a listener") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt new file mode 100644 index 000000000..2f6317d6e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.youtube.misc.audiofocus + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;" + +val pauseOnAudioInterruptPatch = bytecodePatch( + name = "Pause on audio interrupt", + description = "Adds an option to pause playback instead of lowering volume when other audio plays.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + ) + ) + + execute { + addResources("youtube", "misc.audiofocus.pauseOnAudioInterruptPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_pause_on_audio_interrupt"), + ) + + // Hook the builder method that creates AudioFocusRequest. + // At the start, set the willPauseWhenDucked field (b) to true if setting is enabled. + val builderMethod = audioFocusRequestBuilderFingerprint.method + val builderClass = builderMethod.definingClass + + builderMethod.addInstructionsWithLabels( + 0, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldPauseOnAudioInterrupt()Z + move-result v0 + if-eqz v0, :skip_override + const/4 v0, 0x1 + iput-boolean v0, p0, $builderClass->b:Z + """, + ExternalLabel("skip_override", builderMethod.getInstruction(0)), + ) + + // Also hook the audio focus change listener as a backup. + audioFocusChangeListenerFingerprint.method.addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideAudioFocusChange(I)I + move-result p1 + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 993fa820b..f5a39a996 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -23,22 +23,19 @@ import app.revanced.util.InstructionUtils.Companion.writeOpcodes import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode.* +import com.android.tools.smali.dexlib2.analysis.reflection.util.ReflectionUtils +import com.android.tools.smali.dexlib2.formatter.DexFormatter import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.Instruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.* import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.iface.value.* import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.value.* import com.android.tools.smali.dexlib2.util.MethodUtil -import java.util.EnumSet +import java.util.* /** * Starting from and including the instruction at index [startIndex], @@ -180,7 +177,7 @@ internal val Instruction.isReturnInstruction: Boolean * * @param fieldName The name of the field to find. Partial matches are allowed. */ -private fun Method.findInstructionIndexFromToString(fieldName: String) : Int { +private fun Method.findInstructionIndexFromToString(fieldName: String): Int { val stringIndex = indexOfFirstInstruction { val reference = getReference() reference?.string?.contains(fieldName) == true @@ -233,7 +230,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String) : Int { * @param fieldName The name of the field to find. Partial matches are allowed. */ context(BytecodePatchContext) -internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod { +internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { val methodUsageIndex = findInstructionIndexFromToString(fieldName) return navigate(this).to(methodUsageIndex).stop() } @@ -243,7 +240,7 @@ internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod { * * @param fieldName The name of the field to find. Partial matches are allowed. */ -internal fun Method.findFieldFromToString(fieldName: String) : FieldReference { +internal fun Method.findFieldFromToString(fieldName: String): FieldReference { val methodUsageIndex = findInstructionIndexFromToString(fieldName) return getInstruction(methodUsageIndex).getReference()!! } @@ -838,23 +835,59 @@ fun BytecodePatchContext.forEachLiteralValueInstruction( } -private const val RETURN_TYPE_MISMATCH = "Mismatch between override type and Method return type" +private fun MutableMethod.checkReturnType(expectedTypes: Iterable>) { + val returnTypeJava = ReflectionUtils.dexToJavaName(returnType) + check(expectedTypes.any { returnTypeJava == it.name }) { + "Actual return type $returnTypeJava is not contained in expected types: $expectedTypes" + } +} /** - * Overrides the first instruction of a method with a constant `Boolean` return value. + * Overrides the first instruction of a method with returning the default value for the type (or `void`). * None of the method code will ever execute. * - * For methods that return an object or any array type, calling this method with `false` - * will force the method to return a `null` value. - * * @see returnLate */ -fun MutableMethod.returnEarly(value: Boolean = false) { - val returnType = returnType.first() - check(returnType == 'Z' || (!value && (returnType == 'V' || returnType == 'L' || returnType != '['))) { - RETURN_TYPE_MISMATCH +fun MutableMethod.returnEarly() { + val value = when (returnType) { + "V" -> null + "Z" -> ImmutableBooleanEncodedValue.FALSE_VALUE + "B" -> ImmutableByteEncodedValue(0) + "S" -> ImmutableShortEncodedValue(0) + "C" -> ImmutableCharEncodedValue(Char.MIN_VALUE) + "I" -> ImmutableIntEncodedValue(0) + "F" -> ImmutableFloatEncodedValue(0f) + "J" -> ImmutableLongEncodedValue(0) + "D" -> ImmutableDoubleEncodedValue(0.0) + else -> ImmutableNullEncodedValue.INSTANCE } - overrideReturnValue(value.toHexString(), false) + overrideReturnValue(value, false) +} + +private fun MutableMethod.returnString(value: String, late: Boolean) { + checkReturnType(String::class.java.allAssignableTypes()) + overrideReturnValue(ImmutableStringEncodedValue(value), late) +} + +/** + * Overrides the first instruction of a method with a constant `String` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: String) = returnString(value, false) + +/** + * Overrides all return statements with a constant `String` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: String) = returnString(value, true) + +private fun MutableMethod.returnByte(value: Byte, late: Boolean) { + checkReturnType(Byte::class.javaObjectType.allAssignableTypes() + Byte::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableByteEncodedValue(value), late) } /** @@ -863,9 +896,40 @@ fun MutableMethod.returnEarly(value: Boolean = false) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Byte) { - check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) +fun MutableMethod.returnEarly(value: Byte) = returnByte(value, false) + +/** + * Overrides all return statements with a constant `Byte` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Byte) = returnByte(value, true) + +private fun MutableMethod.returnBoolean(value: Boolean, late: Boolean) { + checkReturnType(Boolean::class.javaObjectType.allAssignableTypes() + Boolean::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableBooleanEncodedValue.forBoolean(value), late) +} + +/** + * Overrides the first instruction of a method with a constant `Boolean` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Boolean) = returnBoolean(value, false) + +/** + * Overrides all return statements with a constant `Boolean` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Boolean) = returnBoolean(value, true) + +private fun MutableMethod.returnShort(value: Short, late: Boolean) { + checkReturnType(Short::class.javaObjectType.allAssignableTypes() + Short::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableShortEncodedValue(value), late) } /** @@ -874,9 +938,19 @@ fun MutableMethod.returnEarly(value: Byte) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Short) { - check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) +fun MutableMethod.returnEarly(value: Short) = returnShort(value, false) + +/** + * Overrides all return statements with a constant `Short` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Short) = returnShort(value, true) + +private fun MutableMethod.returnChar(value: Char, late: Boolean) { + checkReturnType(Char::class.javaObjectType.allAssignableTypes() + Char::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableCharEncodedValue(value), late) } /** @@ -885,9 +959,19 @@ fun MutableMethod.returnEarly(value: Short) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Char) { - check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.code.toString(), false) +fun MutableMethod.returnEarly(value: Char) = returnChar(value, false) + +/** + * Overrides all return statements with a constant `Char` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Char) = returnChar(value, true) + +private fun MutableMethod.returnInt(value: Int, late: Boolean) { + checkReturnType(Int::class.javaObjectType.allAssignableTypes() + Int::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableIntEncodedValue(value), late) } /** @@ -896,20 +980,19 @@ fun MutableMethod.returnEarly(value: Char) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Int) { - check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} +fun MutableMethod.returnEarly(value: Int) = returnInt(value, false) /** - * Overrides the first instruction of a method with a constant `Long` return value. - * None of the method code will ever execute. + * Overrides all return statements with a constant `Int` value. + * All method code is executed the same as unpatched. * - * @see returnLate + * @see returnEarly */ -fun MutableMethod.returnEarly(value: Long) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) +fun MutableMethod.returnLate(value: Int) = returnInt(value, true) + +private fun MutableMethod.returnFloat(value: Float, late: Boolean) { + checkReturnType(Float::class.javaObjectType.allAssignableTypes() + Float::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableFloatEncodedValue(value), late) } /** @@ -918,9 +1001,40 @@ fun MutableMethod.returnEarly(value: Long) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Float) { - check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) +fun MutableMethod.returnEarly(value: Float) = returnFloat(value, false) + +/** + * Overrides all return statements with a constant `Float` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Float) = returnFloat(value, true) + +private fun MutableMethod.returnLong(value: Long, late: Boolean) { + checkReturnType(Long::class.javaObjectType.allAssignableTypes() + Long::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableLongEncodedValue(value), late) +} + +/** + * Overrides the first instruction of a method with a constant `Long` return value. + * None of the method code will ever execute. + * + * @see returnLate + */ +fun MutableMethod.returnEarly(value: Long) = returnLong(value, false) + +/** + * Overrides all return statements with a constant `Long` value. + * All method code is executed the same as unpatched. + * + * @see returnEarly + */ +fun MutableMethod.returnLate(value: Long) = returnLong(value, true) + +private fun MutableMethod.returnDouble(value: Double, late: Boolean) { + checkReturnType(Double::class.javaObjectType.allAssignableTypes() + Double::class.javaPrimitiveType!!) + overrideReturnValue(ImmutableDoubleEncodedValue(value), late) } /** @@ -929,113 +1043,7 @@ fun MutableMethod.returnEarly(value: Float) { * * @see returnLate */ -fun MutableMethod.returnEarly(value: Double) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), false) -} - -/** - * Overrides the first instruction of a method with a constant String return value. - * None of the method code will ever execute. - * - * Target method must have return type - * Ljava/lang/String; or Ljava/lang/CharSequence; - * - * @see returnLate - */ -fun MutableMethod.returnEarly(value: String) { - check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(value, false) -} - -/** - * Overrides all return statements with a constant `Boolean` value. - * All method code is executed the same as unpatched. - * - * For methods that return an object or any array type, calling this method with `false` - * will force the method to return a `null` value. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Boolean) { - val returnType = returnType.first() - if (returnType == 'V') { - error("Cannot return late for Method of void type") - } - check(returnType == 'Z' || (!value && (returnType == 'L' || returnType == '['))) { - RETURN_TYPE_MISMATCH - } - - overrideReturnValue(value.toHexString(), true) -} - -/** - * Overrides all return statements with a constant `Byte` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Byte) { - check(returnType.first() == 'B') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} - -/** - * Overrides all return statements with a constant `Short` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Short) { - check(returnType.first() == 'S') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} - -/** - * Overrides all return statements with a constant `Char` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Char) { - check(returnType.first() == 'C') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.code.toString(), true) -} - -/** - * Overrides all return statements with a constant `Int` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Int) { - check(returnType.first() == 'I') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} - -/** - * Overrides all return statements with a constant `Long` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Long) { - check(returnType.first() == 'J') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} - -/** - * Overrides all return statements with a constant `Float` value. - * All method code is executed the same as unpatched. - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: Float) { - check(returnType.first() == 'F') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} +fun MutableMethod.returnEarly(value: Double) = returnDouble(value, false) /** * Overrides all return statements with a constant `Double` value. @@ -1043,75 +1051,164 @@ fun MutableMethod.returnLate(value: Float) { * * @see returnEarly */ -fun MutableMethod.returnLate(value: Double) { - check(returnType.first() == 'D') { RETURN_TYPE_MISMATCH } - overrideReturnValue(value.toString(), true) -} +fun MutableMethod.returnLate(value: Double) = returnDouble(value, true) -/** - * Overrides all return statements with a constant String value. - * All method code is executed the same as unpatched. - * - * Target method must have return type - * Ljava/lang/String; or Ljava/lang/CharSequence; - * - * @see returnEarly - */ -fun MutableMethod.returnLate(value: String) { - check(returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;") { - RETURN_TYPE_MISMATCH - } - overrideReturnValue(value, true) -} - -private fun MutableMethod.overrideReturnValue(value: String, returnLate: Boolean) { - val instructions = if (returnType == "Ljava/lang/String;" || returnType == "Ljava/lang/CharSequence;" ) { - """ - const-string v0, "$value" - return-object v0 - """ - } else when (returnType.first()) { - // If return type is an object, always return null. - 'L', '[' -> { - """ +private fun MutableMethod.overrideReturnValue(value: EncodedValue?, returnLate: Boolean) { + val instructions = if (value == null) { + require(!returnLate) { + "Cannot return late for method of void type" + } + "return-void" + } else { + val encodedValue = DexFormatter.INSTANCE.getEncodedValue(value) + when (value) { + is NullEncodedValue -> { + """ const/4 v0, 0x0 return-object v0 - """ - } + """ + } - 'V' -> { - "return-void" - } + is StringEncodedValue -> { + """ + const-string v0, $encodedValue + return-object v0 + """ + } - 'B', 'Z' -> { - """ - const/4 v0, $value - return v0 - """ - } + is ByteEncodedValue -> { + if (returnType == "B") { + """ + const/4 v0, $encodedValue + return v0 + """ + } else { + """ + const/4 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Byte;->valueOf(B)Ljava/lang/Byte; + move-result-object v0 + return-object v0 + """ + } + } - 'S', 'C' -> { - """ - const/16 v0, $value - return v0 - """ - } + is BooleanEncodedValue -> { + val encodedValue = value.value.toHexString() + if (returnType == "Z") { + """ + const/4 v0, $encodedValue + return v0 + """ + } else { + """ + const/4 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; + move-result-object v0 + return-object v0 + """ + } + } - 'I', 'F' -> { - """ - const v0, $value - return v0 - """ - } + is ShortEncodedValue -> { + if (returnType == "S") { + """ + const/16 v0, $encodedValue + return v0 + """ + } else { + """ + const/16 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Short;->valueOf(S)Ljava/lang/Short; + move-result-object v0 + return-object v0 + """ + } + } - 'J', 'D' -> { - """ - const-wide v0, $value - return-wide v0 - """ - } + is CharEncodedValue -> { + if (returnType == "C") { + """ + const/16 v0, $encodedValue + return v0 + """ + } else { + """ + const/16 v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character; + move-result-object v0 + return-object v0 + """ + } + } - else -> throw Exception("Return type is not supported: $this") + is IntEncodedValue -> { + if (returnType == "I") { + """ + const v0, $encodedValue + return v0 + """ + } else { + """ + const v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer; + move-result-object v0 + return-object v0 + """ + } + } + + is FloatEncodedValue -> { + val encodedValue = "${encodedValue}f" + if (returnType == "F") { + """ + const v0, $encodedValue + return v0 + """ + } else { + """ + const v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Float;->valueOf(F)Ljava/lang/Float; + move-result-object v0 + return-object v0 + """ + } + } + + is LongEncodedValue -> { + val encodedValue = "${encodedValue}L" + if (returnType == "J") { + """ + const-wide v0, $encodedValue + return-wide v0 + """ + } else { + """ + const-wide v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Long;->valueOf(J)Ljava/lang/Long; + move-result-object v0 + return-object v0 + """ + } + } + + is DoubleEncodedValue -> { + if (returnType == "D") { + """ + const-wide v0, $encodedValue + return-wide v0 + """ + } else { + """ + const-wide v0, $encodedValue + invoke-static { v0 }, Ljava/lang/Double;->valueOf(D)Ljava/lang/Double; + move-result-object v0 + return-object v0 + """ + } + } + + else -> throw IllegalArgumentException("Value $value cannot be returned from $this") + } } if (returnLate) { diff --git a/patches/src/main/kotlin/app/revanced/util/Utils.kt b/patches/src/main/kotlin/app/revanced/util/Utils.kt index ef7d0ef1a..6305809ad 100644 --- a/patches/src/main/kotlin/app/revanced/util/Utils.kt +++ b/patches/src/main/kotlin/app/revanced/util/Utils.kt @@ -7,4 +7,21 @@ internal object Utils { .trimIndent() // Remove the leading newline. } -internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0" \ No newline at end of file +internal fun Boolean.toHexString(): String = if (this) "0x1" else "0x0" + +internal fun Class<*>.allAssignableTypes(): Set> { + val result = mutableSetOf>() + + fun visit(child: Class<*>?) { + if (child == null || !result.add(child)) { + return + } + + child.interfaces.forEach(::visit) + visit(child.superclass) + } + + visit(this) + + return result +} diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index ddb6d1d70..9c2a55e0e 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -232,9 +232,6 @@ However, enabling this will also log some user data such as your IP address."Hide compact banners Compact banners are hidden Compact banners are shown - Hide crowdfunding box - Crowdfunding box is hidden - Crowdfunding box is shown Hide expandable card Expandable card under videos is hidden Expandable card under videos is shown @@ -300,8 +297,11 @@ If a Doodle is currently showing in your region and this hide setting is on, the Channel bar is hidden Channel bar is shown Hide channel watermark - Watermark is hidden - Watermark is shown + Channel watermark is hidden + Channel watermark is shown + Hide crowdfunding box + Crowdfunding box is hidden + Crowdfunding box is shown Hide emergency boxes Emergency boxes are hidden Emergency boxes are shown @@ -1558,6 +1558,11 @@ Tap here to learn more about DeArrow" Loop video is on Loop video is off + + Pause on audio interrupt + Playback pauses when other audio plays (e.g. navigation) + Volume lowers when other audio plays + Spoof device dimensions "Device dimensions spoofed diff --git a/settings.gradle.kts b/settings.gradle.kts index 357fde7cd..4ab39e0f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,12 +5,9 @@ pluginManagement { gradlePluginPortal() google() maven { - name = "GitHubPackages" + name = "githubPackages" url = uri("https://maven.pkg.github.com/revanced/registry") - credentials { - username = providers.gradleProperty("gpr.user").getOrElse(System.getenv("GITHUB_ACTOR")) - password = providers.gradleProperty("gpr.key").getOrElse(System.getenv("GITHUB_TOKEN")) - } + credentials(PasswordCredentials::class) } } }