mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-20 01:23:57 +00:00
Compare commits
4 Commits
dev
...
fix/youtub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
debe93d87a | ||
|
|
3910d9c9b9 | ||
|
|
e0edfdbd97 | ||
|
|
95580f84ec |
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -72,7 +72,6 @@ body:
|
|||||||
|
|
||||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
||||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||||
- **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -72,7 +72,6 @@ body:
|
|||||||
|
|
||||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
||||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
||||||
- **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
|
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
9
.github/workflows/build_pull_request.yml
vendored
9
.github/workflows/build_pull_request.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -25,12 +25,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: revanced-patches
|
name: revanced-patches
|
||||||
path: patches/build/libs
|
path: patches/build/libs
|
||||||
|
|||||||
2
.github/workflows/open_pull_request.yml
vendored
2
.github/workflows/open_pull_request.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
|
|||||||
4
.github/workflows/pull_strings.yml
vendored
4
.github/workflows/pull_strings.yml
vendored
@@ -2,7 +2,7 @@ name: Pull strings
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * 0"
|
- cron: "0 */12 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
clean: true
|
clean: true
|
||||||
|
|||||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Preprocess strings
|
- name: Preprocess strings
|
||||||
env:
|
env:
|
||||||
|
|||||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
@@ -31,12 +31,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew :patches:buildAndroid clean
|
run: ./gradlew :patches:buildAndroid clean
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version: 'lts/*'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -52,16 +51,14 @@ jobs:
|
|||||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: cycjimmy/semantic-release-action@v5
|
uses: cycjimmy/semantic-release-action@v4
|
||||||
id: release
|
id: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
|
||||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Attest
|
- name: Attest
|
||||||
if: steps.release.outputs.new_release_published == 'true'
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
uses: actions/attest-build-provenance@v3
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||||
subject-path: patches/build/libs/patches-*.rvp
|
subject-path: patches/build/libs/patches-*.rvp
|
||||||
|
|||||||
2
.github/workflows/update-gradle-wrapper.yml
vendored
2
.github/workflows/update-gradle-wrapper.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update Gradle Wrapper
|
- name: Update Gradle Wrapper
|
||||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
uses: gradle-update/update-gradle-wrapper-action@v1
|
||||||
|
|||||||
1165
CHANGELOG.md
1165
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -97,9 +97,9 @@ Thank you for considering contributing to ReVanced Patches. You can find the con
|
|||||||
|
|
||||||
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||||
|
|
||||||
## 📜 License
|
## 📜 Licence
|
||||||
|
|
||||||
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
||||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
|
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
|
||||||
Any modifications to ReVanced Patches must also be made available under the GPL,
|
Any modifications to ReVanced Patches must also be made available under the GPL,
|
||||||
along with build & install instructions.
|
along with build & install instructions.
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
project_id_env: "CROWDIN_PROJECT_ID"
|
project_id_env: "CROWDIN_PROJECT_ID"
|
||||||
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
api_token_env: "CROWDIN_PERSONAL_TOKEN"
|
||||||
|
|
||||||
preserve_hierarchy: true
|
preserve_hierarchy: false
|
||||||
files:
|
files:
|
||||||
- source: patches/src/main/resources/addresources/values/strings.xml
|
- source: patches/src/main/resources/addresources/values/strings.xml
|
||||||
dest: patches.xml
|
|
||||||
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
|
||||||
skip_untranslated_strings: true
|
skip_untranslated_strings: true
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
aidl = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.google.android.play.core.integrity.protocol;
|
|
||||||
|
|
||||||
interface IExpressIntegrityServiceCallback {
|
|
||||||
oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2;
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.google.android.play.core.integrity.protocol;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
interface IIntegrityServiceCallback {
|
|
||||||
oneway void onResult(in Bundle result) = 1;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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<IBinder> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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<IBinder> binderOverride;
|
|
||||||
|
|
||||||
public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator<IBinder> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.feed;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class LimitFeedToFollowedProfiles {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static Map<String, String> setFollowingHeader(Map<String, String> requestHeaderMap) {
|
|
||||||
String paginationHeaderName = "pagination_source";
|
|
||||||
|
|
||||||
// Patch the header only if it's trying to fetch the default feed
|
|
||||||
String currentHeader = requestHeaderMap.get(paginationHeaderName);
|
|
||||||
if (currentHeader != null && !currentHeader.equals("feed_recs")) {
|
|
||||||
return requestHeaderMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new map as original is unmodifiable.
|
|
||||||
Map<String, String> patchedRequestHeaderMap = new HashMap<>(requestHeaderMap);
|
|
||||||
patchedRequestHeaderMap.put(paginationHeaderName, "following");
|
|
||||||
return patchedRequestHeaderMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.hide.navigation;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class HideNavigationButtonsPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* @param navigationButtonsList the list of navigation buttons, as an (obfuscated) Enum type
|
|
||||||
* @param buttonNameToRemove the name of the button we want to remove
|
|
||||||
* @param enumNameField the field in the nav button enum class which contains the name of the button
|
|
||||||
* @return the patched list of navigation buttons
|
|
||||||
*/
|
|
||||||
public static List<Object> removeNavigationButtonByName(
|
|
||||||
List<Object> navigationButtonsList,
|
|
||||||
String buttonNameToRemove,
|
|
||||||
String enumNameField
|
|
||||||
)
|
|
||||||
throws IllegalAccessException, NoSuchFieldException {
|
|
||||||
for (Object button : navigationButtonsList) {
|
|
||||||
Field f = button.getClass().getDeclaredField(enumNameField);
|
|
||||||
String currentButtonEnumName = (String) f.get(button);
|
|
||||||
|
|
||||||
if (buttonNameToRemove.equals(currentButtonEnumName)) {
|
|
||||||
navigationButtonsList.remove(button);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return navigationButtonsList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.misc.links;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class OpenLinksExternallyPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean openExternally(String url) {
|
|
||||||
try {
|
|
||||||
// The "url" parameter to this function will be of the form.
|
|
||||||
// https://l.instagram.com/?u=<actual url>&e=<tracking id>
|
|
||||||
String actualUrl = Uri.parse(url).getQueryParameter("u");
|
|
||||||
if (actualUrl != null) {
|
|
||||||
Utils.openLink(actualUrl);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "openExternally failure", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.misc.privacy;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class SanitizeSharingLinksPatch {
|
|
||||||
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static String sanitizeSharingLink(String url) {
|
|
||||||
return sanitizer.sanitizeUrlString(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.misc.share.domain;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class ChangeLinkSharingDomainPatch {
|
|
||||||
|
|
||||||
private static String getCustomShareDomain() {
|
|
||||||
// Method is modified during patching.
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static String setCustomShareDomain(String url) {
|
|
||||||
try {
|
|
||||||
Uri uri = Uri.parse(url);
|
|
||||||
Uri.Builder builder = uri
|
|
||||||
.buildUpon()
|
|
||||||
.authority(getCustomShareDomain())
|
|
||||||
.clearQuery();
|
|
||||||
|
|
||||||
String patchedUrl = builder.build().toString();
|
|
||||||
Logger.printInfo(() -> "Domain change from : " + url + " to: " + patchedUrl);
|
|
||||||
return patchedUrl;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "setCustomShareDomain failure with " + url, ex);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package app.revanced.extension.instagram.misc.share.privacy;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class SanitizeSharingLinksPatch {
|
|
||||||
private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static String sanitizeSharingLink(String url) {
|
|
||||||
return sanitizer.sanitizeUrlString(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ForceOriginalAudioPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static void setEnabled() {
|
|
||||||
app.revanced.extension.shared.patches.ForceOriginalAudioPatch.setEnabled(
|
|
||||||
Settings.FORCE_ORIGINAL_AUDIO.get(),
|
|
||||||
Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class HideButtonsPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static int hideCastButton(int original) {
|
|
||||||
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static void hideCastButton(View view) {
|
|
||||||
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static boolean hideHistoryButton(boolean original) {
|
|
||||||
return original && !Settings.HIDE_HISTORY_BUTTON.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static void hideNotificationButton(View view) {
|
|
||||||
if (view.getParent() instanceof ViewGroup viewGroup) {
|
|
||||||
hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
*/
|
|
||||||
public static void hideSearchButton(View view) {
|
|
||||||
hideViewBy0dpUnderCondition(Settings.HIDE_SEARCH_BUTTON, view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
package app.revanced.extension.music.patches;
|
package app.revanced.extension.music.patches;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -12,7 +8,7 @@ public class HideCategoryBarPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point
|
* Injection point
|
||||||
*/
|
*/
|
||||||
public static void hideCategoryBar(View view) {
|
public static boolean hideCategoryBar() {
|
||||||
hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR, view);
|
return Settings.HIDE_CATEGORY_BAR.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package app.revanced.extension.music.patches;
|
|||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class ChangeMiniplayerColorPatch {
|
public class HideUpgradeButtonPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point
|
* Injection point
|
||||||
*/
|
*/
|
||||||
public static boolean changeMiniplayerColor() {
|
public static boolean hideUpgradeButton() {
|
||||||
return Settings.CHANGE_MINIPLAYER_COLOR.get();
|
return Settings.HIDE_UPGRADE_BUTTON.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class NavigationBarPatch {
|
|
||||||
@NonNull
|
|
||||||
private static String lastYTNavigationEnumName = "";
|
|
||||||
|
|
||||||
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
|
|
||||||
if (ytNavigationEnumName != null) {
|
|
||||||
lastYTNavigationEnumName = ytNavigationEnumName.name();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void hideNavigationLabel(TextView textview) {
|
|
||||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void hideNavigationButton(@NonNull View view) {
|
|
||||||
// Hide entire navigation bar.
|
|
||||||
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
|
|
||||||
hideViewUnderCondition(true, (View) view.getParent());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide navigation buttons based on their type.
|
|
||||||
for (NavigationButton button : NavigationButton.values()) {
|
|
||||||
if (button.ytEnumNames.equals(lastYTNavigationEnumName)) {
|
|
||||||
hideViewUnderCondition(button.hidden, view);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum NavigationButton {
|
|
||||||
HOME(
|
|
||||||
"TAB_HOME",
|
|
||||||
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
|
|
||||||
),
|
|
||||||
SAMPLES(
|
|
||||||
"TAB_SAMPLES",
|
|
||||||
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
|
|
||||||
),
|
|
||||||
EXPLORE(
|
|
||||||
"TAB_EXPLORE",
|
|
||||||
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
|
|
||||||
),
|
|
||||||
LIBRARY(
|
|
||||||
"LIBRARY_MUSIC",
|
|
||||||
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
|
|
||||||
),
|
|
||||||
UPGRADE(
|
|
||||||
"TAB_MUSIC_PREMIUM",
|
|
||||||
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
|
|
||||||
);
|
|
||||||
|
|
||||||
private final String ytEnumNames;
|
|
||||||
private final boolean hidden;
|
|
||||||
|
|
||||||
NavigationButton(@NonNull String ytEnumNames, boolean hidden) {
|
|
||||||
this.ytEnumNames = ytEnumNames;
|
|
||||||
this.hidden = hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.extension.music.patches.spoof;
|
package app.revanced.extension.music.patches.spoof;
|
||||||
|
|
||||||
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
|
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||||
@@ -9,6 +8,7 @@ import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofVideoStreamsPatch {
|
public class SpoofVideoStreamsPatch {
|
||||||
@@ -19,12 +19,12 @@ public class SpoofVideoStreamsPatch {
|
|||||||
public static void setClientOrderToUse() {
|
public static void setClientOrderToUse() {
|
||||||
List<ClientType> availableClients = List.of(
|
List<ClientType> availableClients = List.of(
|
||||||
ANDROID_VR_1_43_32,
|
ANDROID_VR_1_43_32,
|
||||||
ANDROID_NO_SDK,
|
ANDROID_VR_1_61_48,
|
||||||
VISIONOS,
|
VISIONOS
|
||||||
ANDROID_VR_1_61_48
|
|
||||||
);
|
);
|
||||||
|
|
||||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse(
|
ClientType client = SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||||
availableClients, SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
|
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setPreferredClient(client);
|
||||||
|
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches.theme;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.theme.BaseThemePatch;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ThemePatch extends BaseThemePatch {
|
|
||||||
|
|
||||||
// Color constants used in relation with litho components.
|
|
||||||
private static final int[] DARK_VALUES = {
|
|
||||||
0xFF212121, // Comments box background.
|
|
||||||
0xFF030303, // Button container background in album.
|
|
||||||
0xFF000000, // Button container background in playlist.
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* <p>
|
|
||||||
* Change the color of Litho components.
|
|
||||||
* If the color of the component matches one of the values, return the background color.
|
|
||||||
*
|
|
||||||
* @param originalValue The original color value.
|
|
||||||
* @return The new or original color value.
|
|
||||||
*/
|
|
||||||
public static int getValue(int originalValue) {
|
|
||||||
return processColorValue(originalValue, DARK_VALUES, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package app.revanced.extension.music.settings;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks GoogleApiActivity to inject a custom ReVancedPreferenceFragment with a toolbar.
|
||||||
|
*/
|
||||||
|
public class GoogleApiActivityHook extends BaseActivityHook {
|
||||||
|
/**
|
||||||
|
* Injection point
|
||||||
|
* <p>
|
||||||
|
* Creates an instance of GoogleApiActivityHook for use in static initialization.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static GoogleApiActivityHook createInstance() {
|
||||||
|
// Must touch the Music settings to ensure the class is loaded and
|
||||||
|
// the values can be found when setting the UI preferences.
|
||||||
|
// Logging anything under non debug ensures this is set.
|
||||||
|
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
||||||
|
|
||||||
|
// YT Music always uses dark mode.
|
||||||
|
Utils.setIsDarkModeEnabled(true);
|
||||||
|
|
||||||
|
return new GoogleApiActivityHook();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fixed theme for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeActivityTheme(Activity activity) {
|
||||||
|
// Override the default YouTube Music theme to increase start padding of list items.
|
||||||
|
// Custom style located in resources/music/values/style.xml
|
||||||
|
activity.setTheme(Utils.getResourceIdentifier("Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for the YouTube Music settings layout.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getContentViewResourceId() {
|
||||||
|
return Utils.getResourceIdentifier("revanced_music_settings_with_toolbar", "layout");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fixed background color for the toolbar.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getToolbarBackgroundColor() {
|
||||||
|
return Utils.getResourceColor("ytm_color_black");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigation icon with a color filter applied.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Drawable getNavigationIcon() {
|
||||||
|
Drawable navigationIcon = ReVancedPreferenceFragment.getBackButtonDrawable();
|
||||||
|
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
|
return navigationIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
||||||
|
return view -> activity.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ReVancedPreferenceFragment for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected PreferenceFragment createPreferenceFragment() {
|
||||||
|
return new ReVancedPreferenceFragment();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
|
||||||
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search.
|
|
||||||
*/
|
|
||||||
public class MusicActivityHook extends BaseActivityHook {
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
public static MusicSearchViewController searchViewController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void initialize(Activity parentActivity) {
|
|
||||||
// Must touch the Music settings to ensure the class is loaded and
|
|
||||||
// the values can be found when setting the UI preferences.
|
|
||||||
// Logging anything under non debug ensures this is set.
|
|
||||||
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
|
||||||
|
|
||||||
// YT Music always uses dark mode.
|
|
||||||
Utils.setIsDarkModeEnabled(true);
|
|
||||||
|
|
||||||
BaseActivityHook.initialize(new MusicActivityHook(), parentActivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the fixed theme for the activity.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void customizeActivityTheme(Activity activity) {
|
|
||||||
// Override the default YouTube Music theme to increase start padding of list items.
|
|
||||||
// Custom style located in resources/music/values/style.xml
|
|
||||||
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
|
||||||
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the resource ID for the YouTube Music settings layout.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected int getContentViewResourceId() {
|
|
||||||
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the fixed background color for the toolbar.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected int getToolbarBackgroundColor() {
|
|
||||||
return Utils.getResourceColor("ytm_color_black");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the navigation icon with a color filter applied.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Drawable getNavigationIcon() {
|
|
||||||
Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable();
|
|
||||||
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
|
||||||
return navigationIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
|
||||||
return view -> {
|
|
||||||
if (searchViewController != null && searchViewController.isSearchActive()) {
|
|
||||||
searchViewController.closeSearch();
|
|
||||||
} else {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds search view components to the toolbar for {@link MusicPreferenceFragment}.
|
|
||||||
*
|
|
||||||
* @param activity The activity hosting the toolbar.
|
|
||||||
* @param toolbar The configured toolbar.
|
|
||||||
* @param fragment The PreferenceFragment associated with the activity.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
|
||||||
if (fragment instanceof MusicPreferenceFragment) {
|
|
||||||
searchViewController = MusicSearchViewController.addSearchViewComponents(
|
|
||||||
activity, toolbar, (MusicPreferenceFragment) fragment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@link MusicPreferenceFragment} for the activity.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected PreferenceFragment createPreferenceFragment() {
|
|
||||||
return new MusicPreferenceFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* <p>
|
|
||||||
* Overrides {@link Activity#finish()} of the injection Activity.
|
|
||||||
*
|
|
||||||
* @return if the original activity finish method should be allowed to run.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static boolean handleFinish() {
|
|
||||||
return MusicSearchViewController.handleFinish(searchViewController);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package app.revanced.extension.music.settings;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
@@ -14,28 +15,15 @@ public class Settings extends BaseSettings {
|
|||||||
// Ads
|
// Ads
|
||||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
||||||
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
|
||||||
|
public static final BooleanSetting HIDE_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_upgrade_button", TRUE, true);
|
||||||
|
|
||||||
// General
|
// General
|
||||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true);
|
|
||||||
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_music_hide_search_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_music_hide_notification_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LIBRARY_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_library_button", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
|
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
|
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true);
|
|
||||||
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||||
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings.preference;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.MusicActivityHook;
|
|
||||||
import app.revanced.extension.shared.GmsCoreSupport;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference fragment for ReVanced settings.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
|
|
||||||
/**
|
|
||||||
* The main PreferenceScreen used to display the current set of preferences.
|
|
||||||
*/
|
|
||||||
private PreferenceScreen preferenceScreen;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the preference fragment.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void initialize() {
|
|
||||||
super.initialize();
|
|
||||||
|
|
||||||
try {
|
|
||||||
preferenceScreen = getPreferenceScreen();
|
|
||||||
Utils.sortPreferenceGroups(preferenceScreen);
|
|
||||||
setPreferenceScreenToolbar(preferenceScreen);
|
|
||||||
|
|
||||||
// Clunky work around until preferences are custom classes that manage themselves.
|
|
||||||
// Custom branding only works with non-root install. But the preferences must be
|
|
||||||
// added during patched because of difficulties detecting during patching if it's
|
|
||||||
// a root install. So instead the non-functional preferences are removed during
|
|
||||||
// runtime if the app is mount (root) installation.
|
|
||||||
if (GmsCoreSupport.isPackageNameOriginal()) {
|
|
||||||
removePreferences(
|
|
||||||
BaseSettings.CUSTOM_BRANDING_ICON.key,
|
|
||||||
BaseSettings.CUSTOM_BRANDING_NAME.key);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the fragment starts.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
try {
|
|
||||||
// Initialize search controller if needed
|
|
||||||
if (MusicActivityHook.searchViewController != null) {
|
|
||||||
// Trigger search data collection after fragment is ready.
|
|
||||||
MusicActivityHook.searchViewController.initializeSearchData();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onStart failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets toolbar for all nested preference screens.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void customizeToolbar(Toolbar toolbar) {
|
|
||||||
MusicActivityHook.setToolbarLayoutParams(toolbar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform actions after toolbar setup.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
|
||||||
if (MusicActivityHook.searchViewController != null
|
|
||||||
&& MusicActivityHook.searchViewController.isSearchActive()) {
|
|
||||||
toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the preference screen for external access by SearchViewController.
|
|
||||||
*/
|
|
||||||
public PreferenceScreen getPreferenceScreenForSearch() {
|
|
||||||
return preferenceScreen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package app.revanced.extension.music.settings.preference;
|
||||||
|
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.GoogleApiActivityHook;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference fragment for ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
|
public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference fragment.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Utils.sortPreferenceGroups(getPreferenceScreen());
|
||||||
|
setPreferenceScreenToolbar(getPreferenceScreen());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeToolbar(Toolbar toolbar) {
|
||||||
|
GoogleApiActivityHook.setToolbarLayoutParams(toolbar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings.search;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
|
||||||
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
|
||||||
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Music-specific search results adapter.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter {
|
|
||||||
|
|
||||||
public MusicSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
|
||||||
BaseSearchViewController.BasePreferenceFragment fragment,
|
|
||||||
BaseSearchViewController searchViewController) {
|
|
||||||
super(context, items, fragment, searchViewController);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected PreferenceScreen getMainPreferenceScreen() {
|
|
||||||
return fragment.getPreferenceScreenForSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings.search;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
|
||||||
import app.revanced.extension.shared.settings.search.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Music-specific search view controller implementation.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class MusicSearchViewController extends BaseSearchViewController {
|
|
||||||
|
|
||||||
public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
|
|
||||||
MusicPreferenceFragment fragment) {
|
|
||||||
return new MusicSearchViewController(activity, toolbar, fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) {
|
|
||||||
super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected BaseSearchResultsAdapter createSearchResultsAdapter() {
|
|
||||||
return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isSpecialPreferenceGroup(Preference preference) {
|
|
||||||
// Music doesn't have SponsorBlock, so no special groups.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
|
|
||||||
// Music doesn't have special preferences.
|
|
||||||
// This method can be empty or handle music-specific preferences if any.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static method for handling Activity finish
|
|
||||||
public static boolean handleFinish(MusicSearchViewController searchViewController) {
|
|
||||||
if (searchViewController != null && searchViewController.isSearchActive()) {
|
|
||||||
searchViewController.closeSearch();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface.
|
|
||||||
private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PreferenceScreen getPreferenceScreenForSearch() {
|
|
||||||
return fragment.getPreferenceScreenForSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView() {
|
|
||||||
return fragment.getView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Activity getActivity() {
|
|
||||||
return fragment.getActivity();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
import com.amazon.video.sdk.player.Player;
|
import com.amazon.video.sdk.player.Player;
|
||||||
|
|
||||||
@@ -65,8 +64,9 @@ public class PlaybackSpeedPatch {
|
|||||||
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||||
speedButton.setImageDrawable(speedIcon);
|
speedButton.setImageDrawable(speedIcon);
|
||||||
|
|
||||||
speedButton.setMinimumWidth(Dim.dp48);
|
int buttonSize = Utils.dipToPixels(48);
|
||||||
speedButton.setMinimumHeight(Dim.dp48);
|
speedButton.setMinimumWidth(buttonSize);
|
||||||
|
speedButton.setMinimumHeight(buttonSize);
|
||||||
|
|
||||||
return speedButton;
|
return speedButton;
|
||||||
}
|
}
|
||||||
@@ -197,11 +197,11 @@ class SpeedIconDrawable extends Drawable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicWidth() {
|
public int getIntrinsicWidth() {
|
||||||
return Dim.dp32;
|
return Utils.dipToPixels(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIntrinsicHeight() {
|
public int getIntrinsicHeight() {
|
||||||
return Dim.dp32;
|
return Utils.dipToPixels(32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
extensions/proguard-rules.pro
vendored
5
extensions/proguard-rules.pro
vendored
@@ -7,3 +7,8 @@
|
|||||||
-keep class com.google.** {
|
-keep class com.google.** {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
-dontwarn java.beans.**
|
||||||
|
-dontwarn jdk.dynalink.**
|
||||||
|
-dontwarn javax.script.**
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
compileOnly(project(":extensions:samsung:radio:stub"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package app.revanced.extension.samsung.radio.misc.fix.crash;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class FixCrashPatch {
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* <p>
|
|
||||||
* Add the required permissions to the request list to avoid crashes on API 34+.
|
|
||||||
**/
|
|
||||||
public static final String[] fixPermissionRequestList(String[] perms) {
|
|
||||||
List<String> permsList = new ArrayList<>(Arrays.asList(perms));
|
|
||||||
if (permsList.contains("android.permission.POST_NOTIFICATIONS")) {
|
|
||||||
permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE"));
|
|
||||||
}
|
|
||||||
if (permsList.contains("android.permission.RECORD_AUDIO")) {
|
|
||||||
permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE");
|
|
||||||
}
|
|
||||||
return permsList.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package app.revanced.extension.samsung.radio.restrictions.device;
|
|
||||||
|
|
||||||
import android.os.SemSystemProperties;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class BypassDeviceChecksPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* <p>
|
|
||||||
* Check if the device has the required hardware
|
|
||||||
**/
|
|
||||||
public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) {
|
|
||||||
String currentDevice = SemSystemProperties.getSalesCode();
|
|
||||||
return Arrays.asList(deviceList).contains(currentDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.android.library)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package android.os;
|
|
||||||
|
|
||||||
public class SemSystemProperties {
|
|
||||||
public static String getSalesCode() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,10 +27,12 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
|
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
||||||
|
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||||
|
|
||||||
private static final String GMS_CORE_PACKAGE_NAME
|
private static final String GMS_CORE_PACKAGE_NAME
|
||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
@@ -50,20 +52,6 @@ public class GmsCoreSupport {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
private static String getOriginalPackageName() {
|
|
||||||
return null; // Modified during patching.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If the current package name is the same as the original unpatched app.
|
|
||||||
* If `GmsCore support` was not included during patching, this returns true;
|
|
||||||
*/
|
|
||||||
public static boolean isPackageNameOriginal() {
|
|
||||||
String originalPackageName = getOriginalPackageName();
|
|
||||||
return originalPackageName == null
|
|
||||||
|| originalPackageName.equals(Utils.getContext().getPackageName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
private static void open(String queryOrLink) {
|
||||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||||
|
|
||||||
@@ -92,17 +80,17 @@ public class GmsCoreSupport {
|
|||||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
context,
|
context,
|
||||||
str("gms_core_dialog_title"), // Title.
|
str("gms_core_dialog_title"), // Title.
|
||||||
str(dialogMessageRef), // Message.
|
str(dialogMessageRef), // Message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
str(positiveButtonTextRef), // OK button text.
|
str(positiveButtonTextRef), // OK button text.
|
||||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||||
null, // No Cancel button action.
|
null, // No Cancel button action.
|
||||||
null, // No Neutral button text.
|
null, // No Neutral button text.
|
||||||
null, // No Neutral button action.
|
null, // No Neutral button action.
|
||||||
true // Dismiss dialog when onNeutralClick.
|
true // Dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
Dialog dialog = dialogPair.first;
|
||||||
@@ -124,10 +112,11 @@ public class GmsCoreSupport {
|
|||||||
// Verify the user has not included GmsCore for a root installation.
|
// Verify the user has not included GmsCore for a root installation.
|
||||||
// GmsCore Support changes the package name, but with a mounted installation
|
// GmsCore Support changes the package name, but with a mounted installation
|
||||||
// all manifest changes are ignored and the original package name is used.
|
// all manifest changes are ignored and the original package name is used.
|
||||||
if (isPackageNameOriginal()) {
|
String packageName = context.getPackageName();
|
||||||
|
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
||||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
||||||
// Cannot use localize text here, since the app will load resources
|
// Cannot use localize text here, since the app will load
|
||||||
// from the unpatched app and all patch strings are missing.
|
// resources from the unpatched app and all patch strings are missing.
|
||||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
// Do not exit. If the app exits before launch completes (and without
|
||||||
@@ -260,7 +249,8 @@ public class GmsCoreSupport {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modified by a patch. Do not touch.
|
||||||
private static String getGmsCoreVendorGroupId() {
|
private static String getGmsCoreVendorGroupId() {
|
||||||
return "app.revanced"; // Modified during patching.
|
return "app.revanced";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**Searches for a group of different patterns using a trie (prefix tree).
|
||||||
* Searches for a group of different patterns using a trie (prefix tree).
|
|
||||||
* Can significantly speed up searching for multiple patterns.
|
* Can significantly speed up searching for multiple patterns.
|
||||||
*/
|
*/
|
||||||
public abstract class TrieSearch<T> {
|
public abstract class TrieSearch<T> {
|
||||||
@@ -56,13 +55,11 @@ public abstract class TrieSearch<T> {
|
|||||||
if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
|
if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
|
||||||
return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
|
return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
|
for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
|
||||||
if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
|
if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback == null || callback.patternMatched(searchText,
|
return callback == null || callback.patternMatched(searchText,
|
||||||
searchTextIndex - patternStartIndex, patternLength, callbackParameter);
|
searchTextIndex - patternStartIndex, patternLength, callbackParameter);
|
||||||
}
|
}
|
||||||
@@ -146,7 +143,6 @@ public abstract class TrieSearch<T> {
|
|||||||
endOfPatternCallback.add(callback);
|
endOfPatternCallback.add(callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leaf != null) {
|
if (leaf != null) {
|
||||||
// Reached end of the graph and a leaf exist.
|
// Reached end of the graph and a leaf exist.
|
||||||
// Recursively call back into this method and push the existing leaf down 1 level.
|
// Recursively call back into this method and push the existing leaf down 1 level.
|
||||||
@@ -161,7 +157,6 @@ public abstract class TrieSearch<T> {
|
|||||||
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final char character = getCharValue(pattern, patternIndex);
|
final char character = getCharValue(pattern, patternIndex);
|
||||||
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
||||||
TrieNode<T> child = children[arrayIndex];
|
TrieNode<T> child = children[arrayIndex];
|
||||||
@@ -186,7 +181,6 @@ public abstract class TrieSearch<T> {
|
|||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
|
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
|
||||||
addNodeToArray(replacement, child);
|
addNodeToArray(replacement, child);
|
||||||
|
|
||||||
boolean collision = false;
|
boolean collision = false;
|
||||||
for (TrieNode<T> existingChild : children) {
|
for (TrieNode<T> existingChild : children) {
|
||||||
if (existingChild != null) {
|
if (existingChild != null) {
|
||||||
@@ -199,7 +193,6 @@ public abstract class TrieSearch<T> {
|
|||||||
if (collision) {
|
if (collision) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
children = replacement;
|
children = replacement;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -239,7 +232,6 @@ public abstract class TrieSearch<T> {
|
|||||||
if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
|
if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
|
||||||
return true; // Leaf exists and it matched the search text.
|
return true; // Leaf exists and it matched the search text.
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
|
List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
|
||||||
if (endOfPatternCallback != null) {
|
if (endOfPatternCallback != null) {
|
||||||
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
||||||
@@ -252,7 +244,6 @@ public abstract class TrieSearch<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrieNode<T>[] children = node.children;
|
TrieNode<T>[] children = node.children;
|
||||||
if (children == null) {
|
if (children == null) {
|
||||||
return false; // Reached a graph end point and there's no further patterns to search.
|
return false; // Reached a graph end point and there's no further patterns to search.
|
||||||
@@ -285,11 +276,9 @@ public abstract class TrieSearch<T> {
|
|||||||
if (leaf != null) {
|
if (leaf != null) {
|
||||||
numberOfPointers += 4; // Number of fields in leaf node.
|
numberOfPointers += 4; // Number of fields in leaf node.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endOfPatternCallback != null) {
|
if (endOfPatternCallback != null) {
|
||||||
numberOfPointers += endOfPatternCallback.size();
|
numberOfPointers += endOfPatternCallback.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
numberOfPointers += children.length;
|
numberOfPointers += children.length;
|
||||||
for (TrieNode<T> child : children) {
|
for (TrieNode<T> child : children) {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.DialogFragment;
|
import android.app.DialogFragment;
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
@@ -14,8 +12,10 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -23,7 +23,12 @@ import android.os.Looper;
|
|||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -32,15 +37,21 @@ import android.view.Window;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.text.Bidi;
|
import java.text.Bidi;
|
||||||
import java.text.Collator;
|
|
||||||
import java.text.Normalizer;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -57,9 +68,7 @@ import app.revanced.extension.shared.settings.AppLanguage;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
@SuppressWarnings("NewApi")
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
@@ -76,15 +85,6 @@ public class Utils {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static Boolean isDarkModeEnabled;
|
private static Boolean isDarkModeEnabled;
|
||||||
|
|
||||||
// Cached Collator instance with its locale.
|
|
||||||
@Nullable
|
|
||||||
private static Locale cachedCollatorLocale;
|
|
||||||
@Nullable
|
|
||||||
private static Collator cachedCollator;
|
|
||||||
|
|
||||||
private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{P}+");
|
|
||||||
private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}");
|
|
||||||
|
|
||||||
private Utils() {
|
private Utils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The version name of the app, such as 20.13.41
|
* @return The version name of the app, such as 19.11.43
|
||||||
*/
|
*/
|
||||||
public static String getAppVersionName() {
|
public static String getAppVersionName() {
|
||||||
if (versionName == null) {
|
if (versionName == null) {
|
||||||
@@ -278,67 +278,41 @@ public class Utils {
|
|||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifier(Context context, String resourceIdentifierName, String type) {
|
||||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
|
|
||||||
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
|
|
||||||
if (resourceId == 0) {
|
|
||||||
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
|
||||||
+ " type: " + type);
|
|
||||||
}
|
|
||||||
return resourceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
* @see #getResourceIdentifierOrThrow(String, String)
|
|
||||||
*/
|
*/
|
||||||
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifier(String resourceIdentifierName, String type) {
|
||||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The resource identifier, or throws an exception if not found.
|
|
||||||
*/
|
|
||||||
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
|
|
||||||
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
|
||||||
if (resourceId == 0) {
|
|
||||||
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
|
||||||
+ " type: " + type);
|
|
||||||
}
|
|
||||||
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 {
|
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
|
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
|
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
|
return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface MatchFilter<T> {
|
public interface MatchFilter<T> {
|
||||||
@@ -349,9 +323,13 @@ public class Utils {
|
|||||||
* Includes sub children.
|
* Includes sub children.
|
||||||
*/
|
*/
|
||||||
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
||||||
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
|
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
||||||
//noinspection unchecked
|
if (child != null) {
|
||||||
return (R) child;
|
//noinspection unchecked
|
||||||
|
return (R) child;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("View with resource name not found: " + str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,9 +415,9 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void setClipboard(CharSequence text) {
|
public static void setClipboard(CharSequence text) {
|
||||||
ClipboardManager clipboard = (ClipboardManager) context
|
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
|
||||||
.getSystemService(Context.CLIPBOARD_SERVICE);
|
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
ClipData clip = ClipData.newPlainText("ReVanced", text);
|
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,13 +577,7 @@ public class Utils {
|
|||||||
showToast(messageToToast, Toast.LENGTH_LONG);
|
showToast(messageToToast, Toast.LENGTH_LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static void showToast(String messageToToast, int toastDuration) {
|
||||||
* Safe to call from any thread.
|
|
||||||
*
|
|
||||||
* @param messageToToast Message to show.
|
|
||||||
* @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}.
|
|
||||||
*/
|
|
||||||
public static void showToast(String messageToToast, int toastDuration) {
|
|
||||||
Objects.requireNonNull(messageToToast);
|
Objects.requireNonNull(messageToToast);
|
||||||
runOnMainThreadNowOrLater(() -> {
|
runOnMainThreadNowOrLater(() -> {
|
||||||
Context currentContext = context;
|
Context currentContext = context;
|
||||||
@@ -707,18 +679,6 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openLink(String url) {
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(url));
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
|
|
||||||
Logger.printInfo(() -> "Opening link with external browser: " + intent);
|
|
||||||
getContext().startActivity(intent);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "openLink failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NetworkType {
|
public enum NetworkType {
|
||||||
NONE,
|
NONE,
|
||||||
MOBILE,
|
MOBILE,
|
||||||
@@ -759,51 +719,436 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides a view by setting its layout width and height to 0dp.
|
* Hide a view by setting its layout params to 0x0
|
||||||
* Handles null layout params safely.
|
* @param view The view to hide.
|
||||||
*
|
|
||||||
* @param view The view to hide. If null, does nothing.
|
|
||||||
*/
|
*/
|
||||||
public static void hideViewByLayoutParams(@Nullable View view) {
|
public static void hideViewByLayoutParams(View view) {
|
||||||
if (view == null) return;
|
if (view instanceof LinearLayout) {
|
||||||
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
|
||||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
view.setLayoutParams(layoutParams);
|
||||||
|
} else if (view instanceof FrameLayout) {
|
||||||
if (params == null) {
|
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
|
||||||
// Create generic 0x0 layout params accepted by all ViewGroups.
|
view.setLayoutParams(layoutParams2);
|
||||||
params = new ViewGroup.LayoutParams(0, 0);
|
} else if (view instanceof RelativeLayout) {
|
||||||
|
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
|
||||||
|
view.setLayoutParams(layoutParams3);
|
||||||
|
} else if (view instanceof Toolbar) {
|
||||||
|
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
|
||||||
|
view.setLayoutParams(layoutParams4);
|
||||||
|
} else if (view instanceof ViewGroup) {
|
||||||
|
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
|
||||||
|
view.setLayoutParams(layoutParams5);
|
||||||
} else {
|
} else {
|
||||||
|
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||||
params.width = 0;
|
params.width = 0;
|
||||||
params.height = 0;
|
params.height = 0;
|
||||||
|
view.setLayoutParams(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
view.setLayoutParams(params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming.
|
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an
|
||||||
* The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP.
|
* optional EditText. The dialog's appearance adapts to the app's dark mode setting, with
|
||||||
* The default dialog background is removed to allow for custom styling.
|
* rounded corners and customizable button actions. Buttons adjust dynamically to their text
|
||||||
|
* content and are arranged in a single row if they fit within 80% of the screen width,
|
||||||
|
* with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
|
||||||
|
* If buttons do not fit, each is placed on a separate row, all aligned to the right.
|
||||||
*
|
*
|
||||||
* @param window The {@link Window} object to configure.
|
* @param context Context used to create the dialog.
|
||||||
* @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}).
|
* @param title Title text of the dialog.
|
||||||
* @param yOffsetDip The vertical offset from the gravity position in DIP.
|
* @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
|
||||||
* @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100).
|
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
||||||
* @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount.
|
* @param okButtonText OK button text, or null to use the default "OK" string.
|
||||||
|
* @param onOkClick Action to perform when the OK button is clicked.
|
||||||
|
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
||||||
|
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
||||||
|
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
||||||
|
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
||||||
|
* @return The Dialog and its main LinearLayout container.
|
||||||
*/
|
*/
|
||||||
public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
|
@SuppressWarnings("ExtractMethodRecommender")
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
public static Pair<Dialog, LinearLayout> createCustomDialog(
|
||||||
|
Context context, String title, CharSequence message, @Nullable EditText editText,
|
||||||
|
String okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
||||||
|
@Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
|
||||||
|
boolean dismissDialogOnNeutralClick
|
||||||
|
) {
|
||||||
|
Logger.printDebug(() -> "Creating custom dialog with title: " + title);
|
||||||
|
|
||||||
params.width = Dim.pctPortraitWidth(widthPercentage);
|
Dialog dialog = new Dialog(context);
|
||||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||||
params.gravity = gravity;
|
|
||||||
params.y = yOffsetDip > 0 ? Dim.dp(yOffsetDip) : 0;
|
// Preset size constants.
|
||||||
if (dimAmount) {
|
final int dip4 = dipToPixels(4);
|
||||||
params.dimAmount = 0f;
|
final int dip8 = dipToPixels(8);
|
||||||
|
final int dip16 = dipToPixels(16);
|
||||||
|
final int dip24 = dipToPixels(24);
|
||||||
|
|
||||||
|
// Create main layout.
|
||||||
|
LinearLayout mainLayout = new LinearLayout(context);
|
||||||
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mainLayout.setPadding(dip24, dip16, dip24, dip24);
|
||||||
|
// Set rounded rectangle background.
|
||||||
|
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
createCornerRadii(28), null, null));
|
||||||
|
mainBackground.getPaint().setColor(getDialogBackgroundColor()); // Dialog background.
|
||||||
|
mainLayout.setBackground(mainBackground);
|
||||||
|
|
||||||
|
// Title.
|
||||||
|
if (!TextUtils.isEmpty(title)) {
|
||||||
|
TextView titleView = new TextView(context);
|
||||||
|
titleView.setText(title);
|
||||||
|
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
titleView.setTextSize(18);
|
||||||
|
titleView.setTextColor(getAppForegroundColor());
|
||||||
|
titleView.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
layoutParams.setMargins(0, 0, 0, dip16);
|
||||||
|
titleView.setLayoutParams(layoutParams);
|
||||||
|
mainLayout.addView(titleView);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setAttributes(params); // Apply window attributes.
|
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
||||||
window.setBackgroundDrawable(null); // Remove default dialog background
|
ScrollView contentScrollView = null;
|
||||||
|
LinearLayout contentContainer;
|
||||||
|
if (message != null || editText != null) {
|
||||||
|
contentScrollView = new ScrollView(context);
|
||||||
|
contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||||
|
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
if (editText != null) {
|
||||||
|
ShapeDrawable scrollViewBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
createCornerRadii(10), null, null));
|
||||||
|
scrollViewBackground.getPaint().setColor(getEditTextBackground());
|
||||||
|
contentScrollView.setPadding(dip8, dip8, dip8, dip8);
|
||||||
|
contentScrollView.setBackground(scrollViewBackground);
|
||||||
|
contentScrollView.setClipToOutline(true);
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
0,
|
||||||
|
1.0f // Weight to take available space.
|
||||||
|
);
|
||||||
|
contentScrollView.setLayoutParams(contentParams);
|
||||||
|
contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
contentScrollView.addView(contentContainer);
|
||||||
|
|
||||||
|
// Message (if not replaced by EditText).
|
||||||
|
if (editText == null) {
|
||||||
|
TextView messageView = new TextView(context);
|
||||||
|
messageView.setText(message); // Supports Spanned (HTML).
|
||||||
|
messageView.setTextSize(16);
|
||||||
|
messageView.setTextColor(getAppForegroundColor());
|
||||||
|
// Enable HTML link clicking if the message contains links.
|
||||||
|
if (message instanceof Spanned) {
|
||||||
|
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams messageParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
messageView.setLayoutParams(messageParams);
|
||||||
|
contentContainer.addView(messageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditText (if provided).
|
||||||
|
if (editText != null) {
|
||||||
|
// Remove EditText from its current parent, if any.
|
||||||
|
ViewGroup parent = (ViewGroup) editText.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(editText);
|
||||||
|
}
|
||||||
|
// Style the EditText to match the dialog theme.
|
||||||
|
editText.setTextColor(getAppForegroundColor());
|
||||||
|
editText.setBackgroundColor(Color.TRANSPARENT);
|
||||||
|
editText.setPadding(0, 0, 0, 0);
|
||||||
|
LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
contentContainer.addView(editText, editTextParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button container.
|
||||||
|
LinearLayout buttonContainer = new LinearLayout(context);
|
||||||
|
buttonContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
buttonContainer.removeAllViews();
|
||||||
|
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
buttonContainerParams.setMargins(0, dip16, 0, 0);
|
||||||
|
buttonContainer.setLayoutParams(buttonContainerParams);
|
||||||
|
|
||||||
|
// Lists to track buttons.
|
||||||
|
List<Button> buttons = new ArrayList<>();
|
||||||
|
List<Integer> buttonWidths = new ArrayList<>();
|
||||||
|
|
||||||
|
// Create buttons in order: Neutral, Cancel, OK.
|
||||||
|
if (neutralButtonText != null && onNeutralClick != null) {
|
||||||
|
Button neutralButton = addButton(
|
||||||
|
context,
|
||||||
|
neutralButtonText,
|
||||||
|
onNeutralClick,
|
||||||
|
false,
|
||||||
|
dismissDialogOnNeutralClick,
|
||||||
|
dialog
|
||||||
|
);
|
||||||
|
buttons.add(neutralButton);
|
||||||
|
neutralButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
buttonWidths.add(neutralButton.getMeasuredWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onCancelClick != null) {
|
||||||
|
Button cancelButton = addButton(
|
||||||
|
context,
|
||||||
|
context.getString(android.R.string.cancel),
|
||||||
|
onCancelClick,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
dialog
|
||||||
|
);
|
||||||
|
buttons.add(cancelButton);
|
||||||
|
cancelButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
buttonWidths.add(cancelButton.getMeasuredWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onOkClick != null) {
|
||||||
|
Button okButton = addButton(
|
||||||
|
context,
|
||||||
|
okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
|
||||||
|
onOkClick,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
dialog
|
||||||
|
);
|
||||||
|
buttons.add(okButton);
|
||||||
|
okButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
buttonWidths.add(okButton.getMeasuredWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle button layout.
|
||||||
|
int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||||
|
int totalWidth = 0;
|
||||||
|
for (Integer width : buttonWidths) {
|
||||||
|
totalWidth += width;
|
||||||
|
}
|
||||||
|
if (buttonWidths.size() > 1) {
|
||||||
|
totalWidth += (buttonWidths.size() - 1) * dip8; // Add margins for gaps.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.size() == 1) {
|
||||||
|
// Single button: stretch to full width.
|
||||||
|
Button singleButton = buttons.get(0);
|
||||||
|
LinearLayout singleContainer = new LinearLayout(context);
|
||||||
|
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
singleContainer.setGravity(Gravity.CENTER);
|
||||||
|
ViewGroup parent = (ViewGroup) singleButton.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(singleButton);
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dipToPixels(36)
|
||||||
|
);
|
||||||
|
params.setMargins(0, 0, 0, 0);
|
||||||
|
singleButton.setLayoutParams(params);
|
||||||
|
singleContainer.addView(singleButton);
|
||||||
|
buttonContainer.addView(singleContainer);
|
||||||
|
} else if (buttons.size() > 1) {
|
||||||
|
// Check if buttons fit in one row.
|
||||||
|
if (totalWidth <= screenWidth * 0.8) {
|
||||||
|
// Single row: Neutral, Cancel, OK.
|
||||||
|
LinearLayout rowContainer = new LinearLayout(context);
|
||||||
|
rowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
rowContainer.setGravity(Gravity.CENTER);
|
||||||
|
rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add all buttons with proportional weights and specific margins.
|
||||||
|
for (int i = 0; i < buttons.size(); i++) {
|
||||||
|
Button button = buttons.get(i);
|
||||||
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(button);
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
dipToPixels(36),
|
||||||
|
buttonWidths.get(i) // Use measured width as weight.
|
||||||
|
);
|
||||||
|
// Set margins based on button type and combination.
|
||||||
|
if (buttons.size() == 2) {
|
||||||
|
// Neutral + OK or Cancel + OK.
|
||||||
|
if (i == 0) { // Neutral or Cancel.
|
||||||
|
params.setMargins(0, 0, dip4, 0);
|
||||||
|
} else { // OK
|
||||||
|
params.setMargins(dip4, 0, 0, 0);
|
||||||
|
}
|
||||||
|
} else if (buttons.size() == 3) {
|
||||||
|
if (i == 0) { // Neutral.
|
||||||
|
params.setMargins(0, 0, dip4, 0);
|
||||||
|
} else if (i == 1) { // Cancel
|
||||||
|
params.setMargins(dip4, 0, dip4, 0);
|
||||||
|
} else { // OK
|
||||||
|
params.setMargins(dip4, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.setLayoutParams(params);
|
||||||
|
rowContainer.addView(button);
|
||||||
|
}
|
||||||
|
buttonContainer.addView(rowContainer);
|
||||||
|
} else {
|
||||||
|
// Multiple rows: OK, Cancel, Neutral.
|
||||||
|
List<Button> reorderedButtons = new ArrayList<>();
|
||||||
|
// Reorder: OK, Cancel, Neutral.
|
||||||
|
if (onOkClick != null) {
|
||||||
|
reorderedButtons.add(buttons.get(buttons.size() - 1));
|
||||||
|
}
|
||||||
|
if (onCancelClick != null) {
|
||||||
|
reorderedButtons.add(buttons.get((neutralButtonText != null && onNeutralClick != null) ? 1 : 0));
|
||||||
|
}
|
||||||
|
if (neutralButtonText != null && onNeutralClick != null) {
|
||||||
|
reorderedButtons.add(buttons.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each button in its own row with spacers.
|
||||||
|
for (int i = 0; i < reorderedButtons.size(); i++) {
|
||||||
|
Button button = reorderedButtons.get(i);
|
||||||
|
LinearLayout singleContainer = new LinearLayout(context);
|
||||||
|
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
singleContainer.setGravity(Gravity.CENTER);
|
||||||
|
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dipToPixels(36)
|
||||||
|
));
|
||||||
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(button);
|
||||||
|
}
|
||||||
|
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dipToPixels(36)
|
||||||
|
);
|
||||||
|
buttonParams.setMargins(0, 0, 0, 0);
|
||||||
|
button.setLayoutParams(buttonParams);
|
||||||
|
singleContainer.addView(button);
|
||||||
|
buttonContainer.addView(singleContainer);
|
||||||
|
|
||||||
|
// Add a spacer between the buttons (except the last one).
|
||||||
|
// Adding a margin between buttons is not suitable, as it conflicts with the single row layout.
|
||||||
|
if (i < reorderedButtons.size() - 1) {
|
||||||
|
View spacer = new View(context);
|
||||||
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dipToPixels(8)
|
||||||
|
);
|
||||||
|
spacer.setLayoutParams(spacerParams);
|
||||||
|
buttonContainer.addView(spacer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ScrollView to main layout only if content exist.
|
||||||
|
if (contentScrollView != null) {
|
||||||
|
mainLayout.addView(contentScrollView);
|
||||||
|
}
|
||||||
|
mainLayout.addView(buttonContainer);
|
||||||
|
dialog.setContentView(mainLayout);
|
||||||
|
|
||||||
|
// Set dialog window attributes.
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
setDialogWindowParameters(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pair<>(dialog, mainLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDialogWindowParameters(Window window) {
|
||||||
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
|
||||||
|
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
||||||
|
int portraitWidth = (int) (displayMetrics.widthPixels * 0.9);
|
||||||
|
|
||||||
|
if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.9);
|
||||||
|
}
|
||||||
|
params.width = portraitWidth;
|
||||||
|
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||||
|
params.gravity = Gravity.CENTER;
|
||||||
|
window.setAttributes(params);
|
||||||
|
window.setBackgroundDrawable(null); // Remove default dialog background.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a styled button to a dialog's button container with customizable text, click behavior, and appearance.
|
||||||
|
* The button's background and text colors adapt to the app's dark mode setting. Buttons stretch to full width
|
||||||
|
* when on separate rows or proportionally based on content when in a single row (Neutral, Cancel, OK order).
|
||||||
|
* When wrapped to separate rows, buttons are ordered OK, Cancel, Neutral.
|
||||||
|
*
|
||||||
|
* @param context Context to create the button and access resources.
|
||||||
|
* @param buttonText Button text to display.
|
||||||
|
* @param onClick Action to perform when the button is clicked, or null if no action is required.
|
||||||
|
* @param isOkButton If this is the OK button, which uses distinct background and text colors.
|
||||||
|
* @param dismissDialog If the dialog should be dismissed when the button is clicked.
|
||||||
|
* @param dialog The Dialog to dismiss when the button is clicked.
|
||||||
|
* @return The created Button.
|
||||||
|
*/
|
||||||
|
private static Button addButton(Context context, String buttonText, Runnable onClick,
|
||||||
|
boolean isOkButton, boolean dismissDialog, Dialog dialog) {
|
||||||
|
Button button = new Button(context, null, 0);
|
||||||
|
button.setText(buttonText);
|
||||||
|
button.setTextSize(14);
|
||||||
|
button.setAllCaps(false);
|
||||||
|
button.setSingleLine(true);
|
||||||
|
button.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||||||
|
button.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
|
||||||
|
int backgroundColor = isOkButton
|
||||||
|
? getOkButtonBackgroundColor() // Background color for OK button (inversion).
|
||||||
|
: getCancelOrNeutralButtonBackgroundColor(); // Background color for Cancel or Neutral buttons.
|
||||||
|
background.getPaint().setColor(backgroundColor);
|
||||||
|
button.setBackground(background);
|
||||||
|
|
||||||
|
button.setTextColor(isDarkModeEnabled()
|
||||||
|
? (isOkButton ? Color.BLACK : Color.WHITE)
|
||||||
|
: (isOkButton ? Color.WHITE : Color.BLACK));
|
||||||
|
|
||||||
|
// Set internal padding.
|
||||||
|
final int dip16 = dipToPixels(16);
|
||||||
|
button.setPadding(dip16, 0, dip16, 0);
|
||||||
|
|
||||||
|
button.setOnClickListener(v -> {
|
||||||
|
if (onClick != null) {
|
||||||
|
onClick.run();
|
||||||
|
}
|
||||||
|
if (dismissDialog) {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of corner radii for a rounded rectangle shape.
|
||||||
|
*
|
||||||
|
* @param dp Radius in density-independent pixels (dip) to apply to all corners.
|
||||||
|
* @return An array of eight float values representing the corner radii
|
||||||
|
* (top-left, top-right, bottom-right, bottom-left).
|
||||||
|
*/
|
||||||
|
public static float[] createCornerRadii(float dp) {
|
||||||
|
final float radius = dipToPixels(dp);
|
||||||
|
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -965,60 +1310,30 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes punctuation and converts text to lowercase. Returns an empty string if input is null.
|
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
||||||
*/
|
*/
|
||||||
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
||||||
if (original == null) return "";
|
if (original == null) return "";
|
||||||
return PUNCTUATION_PATTERN.matcher(original).replaceAll("")
|
return punctuationPattern.matcher(original).replaceAll("")
|
||||||
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes text for search: applies NFD, removes diacritics, and lowercases (locale-neutral).
|
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
||||||
* Returns an empty string if input is null.
|
|
||||||
*/
|
|
||||||
public static String normalizeTextToLowercase(@Nullable CharSequence original) {
|
|
||||||
if (original == null) return "";
|
|
||||||
return DIACRITICS_PATTERN.matcher(Normalizer.normalize(original, Normalizer.Form.NFD))
|
|
||||||
.replaceAll("").toLowerCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a cached Collator for the current locale, or creates a new one if locale changed.
|
|
||||||
*/
|
|
||||||
private static Collator getCollator() {
|
|
||||||
Locale currentLocale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
|
|
||||||
|
|
||||||
if (cachedCollator == null || !currentLocale.equals(cachedCollatorLocale)) {
|
|
||||||
cachedCollatorLocale = currentLocale;
|
|
||||||
cachedCollator = Collator.getInstance(currentLocale);
|
|
||||||
cachedCollator.setStrength(Collator.SECONDARY); // Case-insensitive, diacritic-insensitive.
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedCollator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts a {@link PreferenceGroup} and all nested subgroups by title or key.
|
|
||||||
* <p>
|
|
||||||
* The sort order is controlled by the {@link Sort} suffix present in the preference key.
|
|
||||||
* Preferences without a key or without a {@link Sort} suffix remain in their original order.
|
|
||||||
* <p>
|
|
||||||
* Sorting is performed using {@link Collator} with the current user locale,
|
|
||||||
* ensuring correct alphabetical ordering for all supported languages
|
|
||||||
* (e.g., Ukrainian "і", German "ß", French accented characters, etc.).
|
|
||||||
*
|
*
|
||||||
* @param group the {@link PreferenceGroup} to sort
|
* Sort order is determined by the preferences key {@link Sort} suffix.
|
||||||
|
*
|
||||||
|
* If a preference has no key or no {@link Sort} suffix,
|
||||||
|
* then the preferences are left unsorted.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void sortPreferenceGroups(PreferenceGroup group) {
|
public static void sortPreferenceGroups(PreferenceGroup group) {
|
||||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
||||||
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
||||||
|
|
||||||
// Get cached Collator for locale-aware string comparison.
|
|
||||||
Collator collator = getCollator();
|
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||||
Preference preference = group.getPreference(i);
|
Preference preference = group.getPreference(i);
|
||||||
|
|
||||||
@@ -1049,11 +1364,10 @@ public class Utils {
|
|||||||
preferences.add(new Pair<>(sortValue, preference));
|
preferences.add(new Pair<>(sortValue, preference));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the list using locale-specific collation rules.
|
//noinspection ComparatorCombinators
|
||||||
Collections.sort(preferences, (pair1, pair2)
|
Collections.sort(preferences, (pair1, pair2)
|
||||||
-> collator.compare(pair1.first, pair2.first));
|
-> pair1.first.compareTo(pair2.first));
|
||||||
|
|
||||||
// Reassign order values to reflect the new sorted sequence
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (Pair<String, Preference> pair : preferences) {
|
for (Pair<String, Preference> pair : preferences) {
|
||||||
int order = index++;
|
int order = index++;
|
||||||
@@ -1074,7 +1388,7 @@ public class Utils {
|
|||||||
* Set all preferences to multiline titles if the device is not using an English variant.
|
* Set all preferences to multiline titles if the device is not using an English variant.
|
||||||
* The English strings are heavily scrutinized and all titles fit on screen
|
* The English strings are heavily scrutinized and all titles fit on screen
|
||||||
* except 2 or 3 preference strings and those do not affect readability.
|
* except 2 or 3 preference strings and those do not affect readability.
|
||||||
* <p>
|
*
|
||||||
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
||||||
* and visually it looks better to clip the text and keep all titles 1 line.
|
* and visually it looks better to clip the text and keep all titles 1 line.
|
||||||
*/
|
*/
|
||||||
@@ -1110,6 +1424,42 @@ public class Utils {
|
|||||||
return getResourceColor(colorString);
|
return getResourceColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts dip value to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param dip The density-independent pixels value.
|
||||||
|
* @return The device pixel value.
|
||||||
|
*/
|
||||||
|
public static int dipToPixels(float dip) {
|
||||||
|
return (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dip,
|
||||||
|
Resources.getSystem().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen height to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param percentage The percentage of the screen height (e.g., 30 for 30%).
|
||||||
|
* @return The device pixel value corresponding to the percentage of screen height.
|
||||||
|
*/
|
||||||
|
public static int percentageHeightToPixels(int percentage) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
return (int) (metrics.heightPixels * (percentage / 100.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a percentage of the screen width to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param percentage The percentage of the screen width (e.g., 30 for 30%).
|
||||||
|
* @return The device pixel value corresponding to the percentage of screen width.
|
||||||
|
*/
|
||||||
|
public static int percentageWidthToPixels(int percentage) {
|
||||||
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
return (int) (metrics.widthPixels * (percentage / 100.0f));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
||||||
*/
|
*/
|
||||||
@@ -1147,9 +1497,9 @@ public class Utils {
|
|||||||
blue = Math.round(blue + (255 - blue) * t);
|
blue = Math.round(blue + (255 - blue) * t);
|
||||||
} else {
|
} else {
|
||||||
// Darken or no change: Scale toward black.
|
// Darken or no change: Scale toward black.
|
||||||
red = Math.round(red * factor);
|
red *= factor;
|
||||||
green = Math.round(green * factor);
|
green *= factor;
|
||||||
blue = Math.round(blue * factor);
|
blue *= factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure values are within [0, 255].
|
// Ensure values are within [0, 255].
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.revanced.extension.shared.checks;
|
|||||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -25,7 +26,6 @@ import java.util.Collection;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
abstract class Check {
|
abstract class Check {
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||||
@@ -93,7 +93,7 @@ abstract class Check {
|
|||||||
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
activity,
|
activity,
|
||||||
str("revanced_check_environment_failed_title"), // Title.
|
str("revanced_check_environment_failed_title"), // Title.
|
||||||
message, // Message.
|
message, // Message.
|
||||||
@@ -127,8 +127,7 @@ abstract class Check {
|
|||||||
|
|
||||||
// Add icon to the dialog.
|
// Add icon to the dialog.
|
||||||
ImageView iconView = new ImageView(activity);
|
ImageView iconView = new ImageView(activity);
|
||||||
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
|
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
|
||||||
"revanced_ic_dialog_alert", "drawable"));
|
|
||||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
iconView.setPadding(0, 0, 0, 0);
|
iconView.setPadding(0, 0, 0, 0);
|
||||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||||
@@ -159,8 +158,8 @@ abstract class Check {
|
|||||||
Button ignoreButton;
|
Button ignoreButton;
|
||||||
|
|
||||||
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
||||||
if (buttonContainer.getChildCount() == 1
|
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
|
||||||
&& buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) {
|
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
|
||||||
// Neutral button is the first child (index 0).
|
// Neutral button is the first child (index 0).
|
||||||
ignoreButton = (Button) rowContainer.getChildAt(0);
|
ignoreButton = (Button) rowContainer.getChildAt(0);
|
||||||
// OK button is the last child.
|
// OK button is the last child.
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.GmsCoreSupport;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch shared by YouTube and YT Music.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class CustomBrandingPatch {
|
|
||||||
|
|
||||||
// Important: In the future, additional branding themes can be added but all existing and prior
|
|
||||||
// themes cannot be removed or renamed.
|
|
||||||
//
|
|
||||||
// This is because if a user has a branding theme selected, then only that launch alias is enabled.
|
|
||||||
// If a future update removes or renames that alias, then after updating the app is effectively
|
|
||||||
// broken and it cannot be opened and not even clearing the app data will fix it.
|
|
||||||
// In that situation the only fix is to completely uninstall and reinstall again.
|
|
||||||
//
|
|
||||||
// The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
|
|
||||||
public enum BrandingTheme {
|
|
||||||
/**
|
|
||||||
* Original unpatched icon.
|
|
||||||
*/
|
|
||||||
ORIGINAL,
|
|
||||||
ROUNDED,
|
|
||||||
MINIMAL,
|
|
||||||
SCALED,
|
|
||||||
/**
|
|
||||||
* User provided custom icon.
|
|
||||||
*/
|
|
||||||
CUSTOM;
|
|
||||||
|
|
||||||
private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
|
|
||||||
if (appIndex <= 0) {
|
|
||||||
throw new IllegalArgumentException("App index starts at index 1");
|
|
||||||
}
|
|
||||||
return packageName + ".revanced_" + name().toLowerCase(Locale.US) + '_' + appIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int notificationSmallIcon;
|
|
||||||
|
|
||||||
static {
|
|
||||||
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
|
||||||
if (branding == BrandingTheme.ORIGINAL) {
|
|
||||||
notificationSmallIcon = 0;
|
|
||||||
} else {
|
|
||||||
// Original icon is quantum_ic_video_youtube_white_24
|
|
||||||
String iconName = "revanced_notification_icon";
|
|
||||||
if (branding == BrandingTheme.CUSTOM) {
|
|
||||||
iconName += "_custom";
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationSmallIcon = Utils.getResourceIdentifier(iconName, "drawable");
|
|
||||||
if (notificationSmallIcon == 0) {
|
|
||||||
Logger.printException(() -> "Could not load notification small icon");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static View getLottieViewOrNull(View lottieStartupView) {
|
|
||||||
if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) {
|
|
||||||
return lottieStartupView;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static void setNotificationIcon(Notification.Builder builder) {
|
|
||||||
try {
|
|
||||||
if (notificationSmallIcon != 0) {
|
|
||||||
builder.setSmallIcon(notificationSmallIcon)
|
|
||||||
.setColor(Color.TRANSPARENT); // Remove YT red tint.
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "setNotificationIcon failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* The total number of app name aliases, including dummy aliases.
|
|
||||||
*/
|
|
||||||
private static int numberOfPresetAppNames() {
|
|
||||||
// Modified during patching.
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
public static void setBranding() {
|
|
||||||
try {
|
|
||||||
if (GmsCoreSupport.isPackageNameOriginal()) {
|
|
||||||
Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = Utils.getContext();
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
String packageName = context.getPackageName();
|
|
||||||
|
|
||||||
BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
|
||||||
final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
|
|
||||||
ComponentName componentToEnable = null;
|
|
||||||
ComponentName defaultComponent = null;
|
|
||||||
List<ComponentName> componentsToDisable = new ArrayList<>();
|
|
||||||
|
|
||||||
for (BrandingTheme theme : BrandingTheme.values()) {
|
|
||||||
// Must always update all aliases including custom alias (last index).
|
|
||||||
final int numberOfPresetAppNames = numberOfPresetAppNames();
|
|
||||||
|
|
||||||
// App name indices starts at 1.
|
|
||||||
for (int index = 1; index <= numberOfPresetAppNames; index++) {
|
|
||||||
String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
|
|
||||||
ComponentName component = new ComponentName(packageName, aliasClass);
|
|
||||||
if (defaultComponent == null) {
|
|
||||||
// Default is always the first alias.
|
|
||||||
defaultComponent = component;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == selectedNameIndex && theme == selectedBranding) {
|
|
||||||
componentToEnable = component;
|
|
||||||
} else {
|
|
||||||
componentsToDisable.add(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentToEnable == null) {
|
|
||||||
// User imported a bad app name index value. Either the imported data
|
|
||||||
// was corrupted, or they previously had custom name enabled and the app
|
|
||||||
// no longer has a custom name specified.
|
|
||||||
Utils.showToastLong("Custom branding reset");
|
|
||||||
BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
|
|
||||||
BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
|
|
||||||
|
|
||||||
componentToEnable = defaultComponent;
|
|
||||||
componentsToDisable.remove(defaultComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ComponentName disable : componentsToDisable) {
|
|
||||||
pm.setComponentEnabledSetting(disable,
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use info logging because if the alias status become corrupt the app cannot launch.
|
|
||||||
ComponentName componentToEnableFinal = componentToEnable;
|
|
||||||
Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
|
|
||||||
|
|
||||||
pm.setComponentEnabledSetting(componentToEnable,
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "setBranding failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ForceOriginalAudioPatch {
|
|
||||||
|
|
||||||
private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4";
|
|
||||||
|
|
||||||
private static volatile boolean enabled;
|
|
||||||
|
|
||||||
public static void setEnabled(boolean isEnabled, ClientType client) {
|
|
||||||
enabled = isEnabled;
|
|
||||||
|
|
||||||
if (isEnabled && !client.useAuth && !client.supportsMultiAudioTracks) {
|
|
||||||
// If client spoofing does not use authentication and lacks multi-audio streams,
|
|
||||||
// then can use any language code for the request and if that requested language is
|
|
||||||
// not available YT uses the original audio language. Authenticated requests ignore
|
|
||||||
// the language code and always use the account language. Use a language that is
|
|
||||||
// not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972
|
|
||||||
// but the language is also supported natively by the Meta Quest device that
|
|
||||||
// Android VR is spoofing.
|
|
||||||
AppLanguage override = AppLanguage.NB; // Norwegian Bokmal.
|
|
||||||
Logger.printDebug(() -> "Setting language override: " + override);
|
|
||||||
SpoofVideoStreamsPatch.setLanguageOverride(override);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean ignoreDefaultAudioStream(boolean original) {
|
|
||||||
if (enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) {
|
|
||||||
try {
|
|
||||||
if (!enabled) {
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioTrackId.isEmpty()) {
|
|
||||||
// Older app targets can have empty audio tracks and these might be placeholders.
|
|
||||||
// The real audio tracks are called after these.
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: "
|
|
||||||
+ String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName);
|
|
||||||
|
|
||||||
final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX);
|
|
||||||
if (isOriginal) {
|
|
||||||
Logger.printDebug(() -> "Using audio: " + audioTrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOriginal;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
|
||||||
return isDefault;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* YouTube and YouTube Music.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class SanitizeSharingLinksPatch {
|
|
||||||
|
|
||||||
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
|
||||||
"si",
|
|
||||||
"feature" // Old tracking parameter name, and may be obsolete.
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*/
|
|
||||||
public static String sanitize(String url) {
|
|
||||||
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
|
|
||||||
url = sanitizer.sanitizeUrlString(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
|
||||||
url = url.replace("music.youtube.com", "youtube.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package app.revanced.extension.shared.privacy;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips away specific parameters from URLs.
|
|
||||||
*/
|
|
||||||
public class LinkSanitizer {
|
|
||||||
|
|
||||||
private final Collection<String> parametersToRemove;
|
|
||||||
|
|
||||||
public LinkSanitizer(String ... parametersToRemove) {
|
|
||||||
final int parameterCount = parametersToRemove.length;
|
|
||||||
|
|
||||||
// List is faster if only checking a few parameters.
|
|
||||||
this.parametersToRemove = parameterCount > 4
|
|
||||||
? Set.of(parametersToRemove)
|
|
||||||
: List.of(parametersToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String sanitizeUrlString(String url) {
|
|
||||||
try {
|
|
||||||
return sanitizeUri(Uri.parse(url)).toString();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "sanitizeUrlString failure: " + url, ex);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri sanitizeUri(Uri uri) {
|
|
||||||
try {
|
|
||||||
String scheme = uri.getScheme();
|
|
||||||
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
|
||||||
// Opening YouTube share sheet 'other' option passes the video title as a URI.
|
|
||||||
// Checking !uri.isHierarchical() works for all cases, except if the
|
|
||||||
// video title starts with / and then it's hierarchical but still an invalid URI.
|
|
||||||
Logger.printDebug(() -> "Ignoring uri: " + uri);
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri.Builder builder = uri.buildUpon().clearQuery();
|
|
||||||
|
|
||||||
if (!parametersToRemove.isEmpty()) {
|
|
||||||
for (String paramName : uri.getQueryParameterNames()) {
|
|
||||||
if (!parametersToRemove.contains(paramName)) {
|
|
||||||
for (String value : uri.getQueryParameters(paramName)) {
|
|
||||||
builder.appendQueryParameter(paramName, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri sanitizedUrl = builder.build();
|
|
||||||
Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl);
|
|
||||||
|
|
||||||
return sanitizedUrl;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "sanitizeUri failure: " + uri, ex);
|
|
||||||
return uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,8 +36,8 @@ public enum AppLanguage {
|
|||||||
FR,
|
FR,
|
||||||
GL,
|
GL,
|
||||||
GU,
|
GU,
|
||||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
|
||||||
HI,
|
HI,
|
||||||
|
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
||||||
HR,
|
HR,
|
||||||
HU,
|
HU,
|
||||||
HY,
|
HY,
|
||||||
@@ -60,9 +60,9 @@ public enum AppLanguage {
|
|||||||
MR,
|
MR,
|
||||||
MS,
|
MS,
|
||||||
MY,
|
MY,
|
||||||
NB,
|
|
||||||
NE,
|
NE,
|
||||||
NL,
|
NL,
|
||||||
|
NB,
|
||||||
OR,
|
OR,
|
||||||
PA,
|
PA,
|
||||||
PL,
|
PL,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -15,7 +13,6 @@ import android.widget.Toolbar;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
||||||
@@ -24,15 +21,6 @@ import app.revanced.extension.shared.ui.Dim;
|
|||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
public abstract class BaseActivityHook extends Activity {
|
public abstract class BaseActivityHook extends Activity {
|
||||||
|
|
||||||
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
|
||||||
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
|
||||||
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
|
|
||||||
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
|
|
||||||
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_title", "string");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
||||||
*/
|
*/
|
||||||
@@ -67,27 +55,13 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
|
|
||||||
activity.getFragmentManager()
|
activity.getFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
|
.replace(Utils.getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
|
||||||
.commit();
|
.commit();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* Overrides the ReVanced settings language.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static Context getAttachBaseContext(Context original) {
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (language == AppLanguage.DEFAULT) {
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Utils.getContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +69,8 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
// Replace dummy placeholder toolbar.
|
// Replace dummy placeholder toolbar.
|
||||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
ViewGroup toolBarParent = activity.findViewById(
|
||||||
|
Utils.getResourceIdentifier("revanced_toolbar_parent", "id"));
|
||||||
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
||||||
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
||||||
toolBarParent.removeView(dummyToolbar);
|
toolBarParent.removeView(dummyToolbar);
|
||||||
@@ -107,14 +82,15 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||||
toolbar.setNavigationIcon(getNavigationIcon());
|
toolbar.setNavigationIcon(getNavigationIcon());
|
||||||
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||||
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
toolbar.setTitle(Utils.getResourceIdentifier("revanced_settings_title", "string"));
|
||||||
|
|
||||||
toolbar.setTitleMarginStart(Dim.dp16);
|
final int margin = Utils.dipToPixels(16);
|
||||||
toolbar.setTitleMarginEnd(Dim.dp16);
|
toolbar.setTitleMarginStart(margin);
|
||||||
|
toolbar.setTitleMarginEnd(margin);
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView);
|
||||||
if (toolbarTextView != null) {
|
if (toolbarTextView != null) {
|
||||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||||
toolbarTextView.setTextSize(20);
|
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
}
|
}
|
||||||
setToolbarLayoutParams(toolbar);
|
setToolbarLayoutParams(toolbar);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package app.revanced.extension.shared.settings;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
|
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings shared across multiple apps.
|
* Settings shared across multiple apps.
|
||||||
@@ -25,23 +25,7 @@ public class BaseSettings {
|
|||||||
*/
|
*/
|
||||||
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
||||||
|
|
||||||
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
|
||||||
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
|
|
||||||
|
|
||||||
//
|
|
||||||
// Settings shared by YouTube and YouTube Music.
|
|
||||||
//
|
|
||||||
|
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
|
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
|
||||||
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
|
||||||
|
|
||||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
|
||||||
|
|
||||||
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
|
|
||||||
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
|
|
||||||
|
|
||||||
public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.StringRef;
|
import app.revanced.extension.shared.StringRef;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
public abstract class Setting<T> {
|
public abstract class Setting<T> {
|
||||||
|
|
||||||
@@ -32,66 +23,24 @@ public abstract class Setting<T> {
|
|||||||
*/
|
*/
|
||||||
public interface Availability {
|
public interface Availability {
|
||||||
boolean isAvailable();
|
boolean isAvailable();
|
||||||
|
|
||||||
/**
|
|
||||||
* @return parent settings (dependencies) of this availability.
|
|
||||||
*/
|
|
||||||
default List<Setting<?>> getParentSettings() {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on a single parent setting being enabled.
|
* Availability based on a single parent setting being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parent(BooleanSetting parent) {
|
public static Availability parent(BooleanSetting parent) {
|
||||||
return new Availability() {
|
return parent::get;
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return parent.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Setting<?>> getParentSettings() {
|
|
||||||
return Collections.singletonList(parent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Availability based on a single parent setting being disabled.
|
|
||||||
*/
|
|
||||||
public static Availability parentNot(BooleanSetting parent) {
|
|
||||||
return new Availability() {
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return !parent.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Setting<?>> getParentSettings() {
|
|
||||||
return Collections.singletonList(parent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on all parents being enabled.
|
* Availability based on all parents being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parentsAll(BooleanSetting... parents) {
|
public static Availability parentsAll(BooleanSetting... parents) {
|
||||||
return new Availability() {
|
return () -> {
|
||||||
@Override
|
for (BooleanSetting parent : parents) {
|
||||||
public boolean isAvailable() {
|
if (!parent.get()) return false;
|
||||||
for (BooleanSetting parent : parents) {
|
|
||||||
if (!parent.get()) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Setting<?>> getParentSettings() {
|
|
||||||
return Collections.unmodifiableList(Arrays.asList(parents));
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,19 +48,11 @@ public abstract class Setting<T> {
|
|||||||
* Availability based on any parent being enabled.
|
* Availability based on any parent being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parentsAny(BooleanSetting... parents) {
|
public static Availability parentsAny(BooleanSetting... parents) {
|
||||||
return new Availability() {
|
return () -> {
|
||||||
@Override
|
for (BooleanSetting parent : parents) {
|
||||||
public boolean isAvailable() {
|
if (parent.get()) return true;
|
||||||
for (BooleanSetting parent : parents) {
|
|
||||||
if (parent.get()) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Setting<?>> getParentSettings() {
|
|
||||||
return Collections.unmodifiableList(Arrays.asList(parents));
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +112,6 @@ public abstract class Setting<T> {
|
|||||||
* @return All settings that have been created, sorted by keys.
|
* @return All settings that have been created, sorted by keys.
|
||||||
*/
|
*/
|
||||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||||
//noinspection ComparatorCombinators
|
|
||||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||||
return allLoadedSettings();
|
return allLoadedSettings();
|
||||||
}
|
}
|
||||||
@@ -267,7 +207,9 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
SETTINGS.add(this);
|
SETTINGS.add(this);
|
||||||
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
||||||
Logger.printException(() -> this.getClass().getSimpleName()
|
// Debug setting may not be created yet so using Logger may cause an initialization crash.
|
||||||
|
// Show a toast instead.
|
||||||
|
Utils.showToastLong(this.getClass().getSimpleName()
|
||||||
+ " error: Duplicate Setting key found: " + key);
|
+ " error: Duplicate Setting key found: " + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +231,10 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate an old Setting value previously stored in a different SharedPreference.
|
* Migrate an old Setting value previously stored in a different SharedPreference.
|
||||||
* <p>
|
*
|
||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"rawtypes", "NewApi"})
|
@SuppressWarnings("rawtypes")
|
||||||
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||||
return; // Nothing to do.
|
return; // Nothing to do.
|
||||||
@@ -312,7 +254,7 @@ public abstract class Setting<T> {
|
|||||||
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
||||||
} else {
|
} else {
|
||||||
Logger.printException(() -> "Unknown setting: " + setting);
|
Logger.printException(() -> "Unknown setting: " + setting);
|
||||||
// Remove otherwise it'll show a toast on every launch.
|
// Remove otherwise it'll show a toast on every launch
|
||||||
oldPrefs.preferences.edit().remove(settingKey).apply();
|
oldPrefs.preferences.edit().remove(settingKey).apply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -331,7 +273,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Sets, but does _not_ persistently save the value.
|
* Sets, but does _not_ persistently save the value.
|
||||||
* This method is only to be used by the Settings preference code.
|
* This method is only to be used by the Settings preference code.
|
||||||
* <p>
|
*
|
||||||
* This intentionally is a static method to deter
|
* This intentionally is a static method to deter
|
||||||
* accidental usage when {@link #save(Object)} was intended.
|
* accidental usage when {@link #save(Object)} was intended.
|
||||||
*/
|
*/
|
||||||
@@ -407,17 +349,6 @@ public abstract class Setting<T> {
|
|||||||
return availability == null || availability.isAvailable();
|
return availability == null || availability.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parent Settings that this setting depends on.
|
|
||||||
* @return List of parent Settings, or empty list if no dependencies exist.
|
|
||||||
* Defensive: handles null availability or missing getParentSettings() override.
|
|
||||||
*/
|
|
||||||
public List<Setting<?>> getParentSettings() {
|
|
||||||
return availability == null
|
|
||||||
? Collections.emptyList()
|
|
||||||
: Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the currently set value is the same as {@link #defaultValue}
|
* @return if the currently set value is the same as {@link #defaultValue}
|
||||||
*/
|
*/
|
||||||
@@ -536,12 +467,9 @@ public abstract class Setting<T> {
|
|||||||
callback.settingsImported(alertDialogContext);
|
callback.settingsImported(alertDialogContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a delay, otherwise the toast can move about on screen from the dismissing dialog.
|
Utils.showToastLong(numberOfSettingsImported == 0
|
||||||
final int numberOfSettingsImportedFinal = numberOfSettingsImported;
|
? str("revanced_settings_import_reset")
|
||||||
Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0
|
: str("revanced_settings_import_success", numberOfSettingsImported));
|
||||||
? str("revanced_settings_import_reset")
|
|
||||||
: str("revanced_settings_import_success", numberOfSettingsImportedFinal)),
|
|
||||||
150);
|
|
||||||
|
|
||||||
return rebootSettingChanged;
|
return rebootSettingChanged;
|
||||||
} catch (JSONException | IllegalArgumentException ex) {
|
} catch (JSONException | IllegalArgumentException ex) {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import app.revanced.extension.shared.Utils;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
@@ -53,7 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
* Set by subclasses if Strings cannot be added as a resource.
|
* Set by subclasses if Strings cannot be added as a resource.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
protected static CharSequence restartDialogTitle, restartDialogMessage, restartDialogButtonText, confirmDialogTitle;
|
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||||
try {
|
try {
|
||||||
@@ -125,13 +124,10 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
showingUserDialogMessage = true;
|
showingUserDialogMessage = true;
|
||||||
|
|
||||||
CharSequence message = BulletPointPreference.formatIntoBulletPoints(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
Objects.requireNonNull(setting.userDialogMessage).toString());
|
|
||||||
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
|
||||||
context,
|
context,
|
||||||
confirmDialogTitle, // Title.
|
confirmDialogTitle, // Title.
|
||||||
message,
|
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
null, // OK button text.
|
null, // OK button text.
|
||||||
() -> {
|
() -> {
|
||||||
@@ -155,7 +151,6 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
);
|
);
|
||||||
|
|
||||||
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
|
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
|
||||||
dialogPair.first.setCancelable(false);
|
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
dialogPair.first.show();
|
dialogPair.first.show();
|
||||||
@@ -253,8 +248,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||||
}
|
}
|
||||||
updateListPreferenceSummary(listPref, setting);
|
updateListPreferenceSummary(listPref, setting);
|
||||||
} else if (!pref.getClass().equals(Preference.class)) {
|
} else {
|
||||||
// Ignore root preference class because there is no data to sync.
|
|
||||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,8 +302,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
restartDialogButtonText = str("revanced_settings_restart");
|
restartDialogButtonText = str("revanced_settings_restart");
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
|
||||||
context,
|
|
||||||
restartDialogTitle, // Title.
|
restartDialogTitle, // Title.
|
||||||
restartDialogMessage, // Message.
|
restartDialogMessage, // Message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.SpannedString;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.style.BulletSpan;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the summary text bullet points into Spanned text for better presentation.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
|
||||||
public class BulletPointPreference extends Preference {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces bullet points with styled spans.
|
|
||||||
*/
|
|
||||||
public static CharSequence formatIntoBulletPoints(CharSequence source) {
|
|
||||||
final char bulletPoint = '•';
|
|
||||||
if (TextUtils.indexOf(source, bulletPoint) < 0) {
|
|
||||||
return source; // Nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder(source);
|
|
||||||
|
|
||||||
int lineStart = 0;
|
|
||||||
int length = builder.length();
|
|
||||||
|
|
||||||
while (lineStart < length) {
|
|
||||||
int lineEnd = TextUtils.indexOf(builder, '\n', lineStart);
|
|
||||||
if (lineEnd < 0) lineEnd = length;
|
|
||||||
|
|
||||||
// Apply BulletSpan only if the line starts with the '•' character.
|
|
||||||
if (lineEnd > lineStart && builder.charAt(lineStart) == bulletPoint) {
|
|
||||||
int deleteEnd = lineStart + 1; // remove the bullet itself
|
|
||||||
|
|
||||||
// If there's a single space right after the bullet, remove that too.
|
|
||||||
if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') {
|
|
||||||
deleteEnd++;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.delete(lineStart, deleteEnd);
|
|
||||||
|
|
||||||
// Apply the BulletSpan to the remainder of that line.
|
|
||||||
builder.setSpan(new BulletSpan(20),
|
|
||||||
lineStart,
|
|
||||||
lineEnd - (deleteEnd - lineStart), // adjust for deleted chars.
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update total length and lineEnd after deletion.
|
|
||||||
length = builder.length();
|
|
||||||
final int removed = deleteEnd - lineStart;
|
|
||||||
lineEnd -= removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SpannedString(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
super.setSummary(formatIntoBulletPoints(summary));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the summary text bullet points into Spanned text for better presentation.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
|
||||||
public class BulletPointSwitchPreference extends SwitchPreference {
|
|
||||||
|
|
||||||
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointSwitchPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BulletPointSwitchPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
super.setSummary(formatIntoBulletPoints(summary));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummaryOn(CharSequence summaryOn) {
|
|
||||||
super.setSummaryOn(formatIntoBulletPoints(summaryOn));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummaryOff(CharSequence summaryOff) {
|
|
||||||
super.setSummaryOff(formatIntoBulletPoints(summaryOff));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -12,20 +13,20 @@ import android.os.Bundle;
|
|||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
import android.widget.EditText;
|
import android.widget.*;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -34,9 +35,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
import app.revanced.extension.shared.ui.ColorDot;
|
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||||
@@ -45,98 +43,100 @@ import app.revanced.extension.shared.ui.Dim;
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ColorPickerPreference extends EditTextPreference {
|
public class ColorPickerPreference extends EditTextPreference {
|
||||||
/** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */
|
|
||||||
public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7;
|
|
||||||
public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9;
|
|
||||||
|
|
||||||
/** Matches everything that is not a hex number/letter. */
|
/**
|
||||||
|
* Character to show the color appearance.
|
||||||
|
*/
|
||||||
|
public static final String COLOR_DOT_STRING = "⬤";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length of a valid color string of format #RRGGBB.
|
||||||
|
*/
|
||||||
|
public static final int COLOR_STRING_LENGTH = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches everything that is not a hex number/letter.
|
||||||
|
*/
|
||||||
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
||||||
|
|
||||||
/** Alpha for dimming when the preference is disabled. */
|
/**
|
||||||
public static final float DISABLED_ALPHA = 0.5f; // 50%
|
* Alpha for dimming when the preference is disabled.
|
||||||
|
*/
|
||||||
|
private static final float DISABLED_ALPHA = 0.5f; // 50%
|
||||||
|
|
||||||
/** View displaying a colored dot in the widget area. */
|
/**
|
||||||
|
* View displaying a colored dot in the widget area.
|
||||||
|
*/
|
||||||
private View widgetColorDot;
|
private View widgetColorDot;
|
||||||
|
|
||||||
/** Dialog View displaying a colored dot for the selected color preview in the dialog. */
|
/**
|
||||||
private View dialogColorDot;
|
* Current color in RGB format (without alpha).
|
||||||
|
*/
|
||||||
/** Current color, including alpha channel if opacity slider is enabled. */
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int currentColor;
|
private int currentColor;
|
||||||
|
|
||||||
/** Associated setting for storing the color value. */
|
/**
|
||||||
|
* Associated setting for storing the color value.
|
||||||
|
*/
|
||||||
private StringSetting colorSetting;
|
private StringSetting colorSetting;
|
||||||
|
|
||||||
/** Dialog TextWatcher for the EditText to monitor color input changes. */
|
/**
|
||||||
|
* Dialog TextWatcher for the EditText to monitor color input changes.
|
||||||
|
*/
|
||||||
private TextWatcher colorTextWatcher;
|
private TextWatcher colorTextWatcher;
|
||||||
|
|
||||||
/** Dialog color picker view. */
|
/**
|
||||||
protected ColorPickerView dialogColorPickerView;
|
* Dialog TextView displaying a colored dot for the selected color preview in the dialog.
|
||||||
|
*/
|
||||||
|
private TextView dialogColorPreview;
|
||||||
|
|
||||||
/** Listener for color changes. */
|
/**
|
||||||
protected OnColorChangeListener colorChangeListener;
|
* Dialog color picker view.
|
||||||
|
*/
|
||||||
/** Whether the opacity slider is enabled. */
|
private ColorPickerView dialogColorPickerView;
|
||||||
private boolean opacitySliderEnabled = false;
|
|
||||||
|
|
||||||
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
|
|
||||||
getResourceIdentifierOrThrow("revanced_color_picker_view", "id");
|
|
||||||
public static final int ID_PREFERENCE_COLOR_DOT =
|
|
||||||
getResourceIdentifierOrThrow("preference_color_dot", "id");
|
|
||||||
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
|
|
||||||
getResourceIdentifierOrThrow("revanced_color_dot_widget", "layout");
|
|
||||||
public static final int LAYOUT_REVANCED_COLOR_PICKER =
|
|
||||||
getResourceIdentifierOrThrow("revanced_color_picker", "layout");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes non valid hex characters, converts to all uppercase,
|
* Removes non valid hex characters, converts to all uppercase,
|
||||||
* and adds # character to the start if not present.
|
* and adds # character to the start if not present.
|
||||||
*/
|
*/
|
||||||
public static String cleanupColorCodeString(String colorString, boolean includeAlpha) {
|
public static String cleanupColorCodeString(String colorString) {
|
||||||
|
// Remove non-hex chars, convert to uppercase, and ensure correct length
|
||||||
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
||||||
.replaceAll("").toUpperCase(Locale.ROOT);
|
.replaceAll("").toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
if (result.length() < COLOR_STRING_LENGTH) {
|
||||||
if (result.length() < maxLength) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.substring(0, maxLength);
|
return result.substring(0, COLOR_STRING_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param color Color, with or without alpha channel.
|
* @param color RGB color, without an alpha channel.
|
||||||
* @param includeAlpha Whether to include the alpha channel in the output string.
|
* @return #RRGGBB hex color string
|
||||||
* @return #RRGGBB or #AARRGGBB hex color string
|
|
||||||
*/
|
*/
|
||||||
public static String getColorString(@ColorInt int color, boolean includeAlpha) {
|
public static String getColorString(@ColorInt int color) {
|
||||||
if (includeAlpha) {
|
String colorString = String.format("#%06X", color);
|
||||||
return String.format("#%08X", color);
|
if ((color & 0xFF000000) != 0) {
|
||||||
|
// Likely a bug somewhere.
|
||||||
|
Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
|
||||||
}
|
}
|
||||||
color = color & 0x00FFFFFF; // Mask to strip alpha.
|
return colorString;
|
||||||
return String.format("#%06X", color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for notifying color changes.
|
* Creates a Spanned object for a colored dot using SpannableString.
|
||||||
|
*
|
||||||
|
* @param color The RGB color (without alpha).
|
||||||
|
* @return A Spanned object with the colored dot.
|
||||||
*/
|
*/
|
||||||
public interface OnColorChangeListener {
|
public static Spanned getColorDot(@ColorInt int color) {
|
||||||
void onColorChanged(String key, int newColor);
|
SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
|
||||||
}
|
spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
/**
|
spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
||||||
* Sets the listener for color changes.
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
*/
|
return spannable;
|
||||||
public void setOnColorChangeListener(OnColorChangeListener listener) {
|
|
||||||
this.colorChangeListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables or disables the opacity slider in the color picker dialog.
|
|
||||||
*/
|
|
||||||
public void setOpacitySliderEnabled(boolean enabled) {
|
|
||||||
this.opacitySliderEnabled = enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ColorPickerPreference(Context context) {
|
public ColorPickerPreference(Context context) {
|
||||||
@@ -158,13 +158,9 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
||||||
*/
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
if (getKey() != null) {
|
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
||||||
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
if (colorSetting == null) {
|
||||||
if (colorSetting == null) {
|
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
||||||
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.printDebug(() -> "initialized without key, settings will be loaded later");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
@@ -175,29 +171,27 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the widget layout to a custom layout containing the colored dot.
|
// Set the widget layout to a custom layout containing the colored dot.
|
||||||
setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
|
setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected color and updates the UI and settings.
|
* Sets the selected color and updates the UI and settings.
|
||||||
|
*
|
||||||
|
* @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
|
||||||
|
* @throws IllegalArgumentException If the color string is invalid.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setText(String colorString) {
|
public final void setText(String colorString) {
|
||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "setText: " + colorString);
|
Logger.printDebug(() -> "setText: " + colorString);
|
||||||
super.setText(colorString);
|
super.setText(colorString);
|
||||||
|
|
||||||
currentColor = Color.parseColor(colorString);
|
currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
||||||
if (colorSetting != null) {
|
if (colorSetting != null) {
|
||||||
colorSetting.save(getColorString(currentColor, opacitySliderEnabled));
|
colorSetting.save(getColorString(currentColor));
|
||||||
}
|
}
|
||||||
updateDialogColorDot();
|
updateColorPreview();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
|
|
||||||
// Notify the listener about the color change.
|
|
||||||
if (colorChangeListener != null) {
|
|
||||||
colorChangeListener.onColorChanged(getKey(), currentColor);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
// This code is reached if the user pastes settings json with an invalid color
|
// This code is reached if the user pastes settings json with an invalid color
|
||||||
// since this preference is updated with the new setting text.
|
// since this preference is updated with the new setting text.
|
||||||
@@ -209,8 +203,38 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindView(View view) {
|
||||||
|
super.onBindView(view);
|
||||||
|
|
||||||
|
widgetColorDot = view.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_color_dot_widget", "id"));
|
||||||
|
widgetColorDot.setBackgroundResource(getResourceIdentifier(
|
||||||
|
"revanced_settings_circle_background", "drawable"));
|
||||||
|
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||||
|
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the color preview TextView with a colored dot.
|
||||||
|
*/
|
||||||
|
private void updateColorPreview() {
|
||||||
|
if (dialogColorPreview != null) {
|
||||||
|
dialogColorPreview.setText(getColorDot(currentColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetColorDot() {
|
||||||
|
if (widgetColorDot != null) {
|
||||||
|
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||||
|
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
||||||
|
*
|
||||||
|
* @return A TextWatcher that updates the color preview on valid input.
|
||||||
*/
|
*/
|
||||||
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
||||||
return new TextWatcher() {
|
return new TextWatcher() {
|
||||||
@@ -226,16 +250,15 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
public void afterTextChanged(Editable edit) {
|
public void afterTextChanged(Editable edit) {
|
||||||
try {
|
try {
|
||||||
String colorString = edit.toString();
|
String colorString = edit.toString();
|
||||||
String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled);
|
|
||||||
|
String sanitizedColorString = cleanupColorCodeString(colorString);
|
||||||
if (!sanitizedColorString.equals(colorString)) {
|
if (!sanitizedColorString.equals(colorString)) {
|
||||||
edit.replace(0, colorString.length(), sanitizedColorString);
|
edit.replace(0, colorString.length(), sanitizedColorString);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int expectedLength = opacitySliderEnabled
|
if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
|
||||||
? COLOR_STRING_LENGTH_WITH_ALPHA
|
// User is still typing out the color.
|
||||||
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
|
||||||
if (sanitizedColorString.length() != expectedLength) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +266,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
if (currentColor != newColor) {
|
if (currentColor != newColor) {
|
||||||
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
||||||
currentColor = newColor;
|
currentColor = newColor;
|
||||||
updateDialogColorDot();
|
updateColorPreview();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
colorPickerView.setColor(newColor);
|
colorPickerView.setColor(newColor);
|
||||||
}
|
}
|
||||||
@@ -256,65 +279,32 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for subclasses to add a custom view to the top of the dialog.
|
* Creates a Dialog with a color preview and EditText for hex color input.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
|
||||||
protected View createExtraDialogContentView(Context context) {
|
|
||||||
return null; // Default implementation returns no extra view.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for subclasses to handle the OK button click.
|
|
||||||
*/
|
|
||||||
protected void onDialogOkClicked() {
|
|
||||||
// Default implementation does nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for subclasses to handle the Neutral button click.
|
|
||||||
*/
|
|
||||||
protected void onDialogNeutralClicked() {
|
|
||||||
// Default implementation.
|
|
||||||
try {
|
|
||||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue);
|
|
||||||
dialogColorPickerView.setColor(defaultColor);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Reset button failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
// Create content container for all dialog views.
|
|
||||||
LinearLayout contentContainer = new LinearLayout(context);
|
|
||||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
|
|
||||||
// Add extra view from subclass if it exists.
|
|
||||||
View extraView = createExtraDialogContentView(context);
|
|
||||||
if (extraView != null) {
|
|
||||||
contentContainer.addView(extraView);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inflate color picker view.
|
// Inflate color picker view.
|
||||||
View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null);
|
View colorPicker = LayoutInflater.from(context).inflate(
|
||||||
dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW);
|
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||||
dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled);
|
dialogColorPickerView = colorPicker.findViewById(
|
||||||
|
getResourceIdentifier("revanced_color_picker_view", "id"));
|
||||||
dialogColorPickerView.setColor(currentColor);
|
dialogColorPickerView.setColor(currentColor);
|
||||||
contentContainer.addView(colorPicker);
|
|
||||||
|
|
||||||
// Horizontal layout for preview and EditText.
|
// Horizontal layout for preview and EditText.
|
||||||
LinearLayout inputLayout = new LinearLayout(context);
|
LinearLayout inputLayout = new LinearLayout(context);
|
||||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
|
|
||||||
dialogColorDot = new View(context);
|
dialogColorPreview = new TextView(context);
|
||||||
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(Dim.dp20,Dim.dp20);
|
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||||
previewParams.setMargins(Dim.dp16, 0, Dim.dp10, 0);
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
dialogColorDot.setLayoutParams(previewParams);
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
inputLayout.addView(dialogColorDot);
|
);
|
||||||
updateDialogColorDot();
|
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
|
||||||
|
dialogColorPreview.setLayoutParams(previewParams);
|
||||||
|
inputLayout.addView(dialogColorPreview);
|
||||||
|
updateColorPreview();
|
||||||
|
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
ViewParent parent = editText.getParent();
|
ViewParent parent = editText.getParent();
|
||||||
@@ -325,7 +315,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
));
|
));
|
||||||
String currentColorString = getColorString(currentColor, opacitySliderEnabled);
|
String currentColorString = getColorString(currentColor);
|
||||||
editText.setText(currentColorString);
|
editText.setText(currentColorString);
|
||||||
editText.setSelection(currentColorString.length());
|
editText.setSelection(currentColorString.length());
|
||||||
editText.setTypeface(Typeface.MONOSPACE);
|
editText.setTypeface(Typeface.MONOSPACE);
|
||||||
@@ -344,12 +334,16 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
paddingView.setLayoutParams(params);
|
paddingView.setLayoutParams(params);
|
||||||
inputLayout.addView(paddingView);
|
inputLayout.addView(paddingView);
|
||||||
|
|
||||||
|
// Create content container for color picker and input layout.
|
||||||
|
LinearLayout contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
contentContainer.addView(colorPicker);
|
||||||
contentContainer.addView(inputLayout);
|
contentContainer.addView(inputLayout);
|
||||||
|
|
||||||
// Create ScrollView to wrap the content container.
|
// Create ScrollView to wrap the content container.
|
||||||
ScrollView contentScrollView = new ScrollView(context);
|
ScrollView contentScrollView = new ScrollView(context);
|
||||||
contentScrollView.setVerticalScrollBarEnabled(false);
|
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
||||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
||||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
0,
|
0,
|
||||||
@@ -358,43 +352,51 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
contentScrollView.setLayoutParams(scrollViewParams);
|
contentScrollView.setLayoutParams(scrollViewParams);
|
||||||
contentScrollView.addView(contentContainer);
|
contentScrollView.addView(contentContainer);
|
||||||
|
|
||||||
final int originalColor = currentColor;
|
// Create custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
final int originalColor = currentColor & 0x00FFFFFF;
|
||||||
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"),
|
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
|
||||||
null,
|
null, // No message.
|
||||||
null,
|
null, // No EditText.
|
||||||
null,
|
null, // OK button text.
|
||||||
() -> { // OK button action.
|
() -> {
|
||||||
|
// OK button action.
|
||||||
try {
|
try {
|
||||||
String colorString = editText.getText().toString();
|
String colorString = editText.getText().toString();
|
||||||
int expectedLength = opacitySliderEnabled
|
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||||
? COLOR_STRING_LENGTH_WITH_ALPHA
|
|
||||||
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
|
||||||
if (colorString.length() != expectedLength) {
|
|
||||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
setText(getColorString(originalColor, opacitySliderEnabled));
|
setText(getColorString(originalColor));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setText(colorString);
|
setText(colorString);
|
||||||
|
|
||||||
onDialogOkClicked();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Should never happen due to a bad color string,
|
// Should never happen due to a bad color string,
|
||||||
// since the text is validated and fixed while the user types.
|
// since the text is validated and fixed while the user types.
|
||||||
Logger.printException(() -> "OK button failure", ex);
|
Logger.printException(() -> "OK button failure", ex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() -> { // Cancel button action.
|
() -> {
|
||||||
|
// Cancel button action.
|
||||||
try {
|
try {
|
||||||
setText(getColorString(originalColor, opacitySliderEnabled));
|
// Restore the original color.
|
||||||
|
setText(getColorString(originalColor));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Cancel button failure", ex);
|
Logger.printException(() -> "Cancel button failure", ex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
str("revanced_settings_reset_color"), // Neutral button text.
|
str("revanced_settings_reset_color"), // Neutral button text.
|
||||||
this::onDialogNeutralClicked, // Neutral button action.
|
() -> {
|
||||||
false // Do not dismiss dialog.
|
// Neutral button action.
|
||||||
|
try {
|
||||||
|
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||||
|
// Setting view color causes listener callback into this class.
|
||||||
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Reset button failure", ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false // Do not dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add the ScrollView to the dialog's main layout.
|
// Add the ScrollView to the dialog's main layout.
|
||||||
@@ -410,13 +412,13 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String updatedColorString = getColorString(color, opacitySliderEnabled);
|
String updatedColorString = getColorString(color);
|
||||||
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||||
currentColor = color;
|
currentColor = color;
|
||||||
editText.setText(updatedColorString);
|
editText.setText(updatedColorString);
|
||||||
editText.setSelection(updatedColorString.length());
|
editText.setSelection(updatedColorString.length());
|
||||||
|
|
||||||
updateDialogColorDot();
|
updateColorPreview();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,7 +437,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
colorTextWatcher = null;
|
colorTextWatcher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogColorDot = null;
|
dialogColorPreview = null;
|
||||||
dialogColorPickerView = null;
|
dialogColorPickerView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,32 +446,4 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
super.setEnabled(enabled);
|
super.setEnabled(enabled);
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBindView(View view) {
|
|
||||||
super.onBindView(view);
|
|
||||||
|
|
||||||
widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
|
||||||
updateWidgetColorDot();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateWidgetColorDot() {
|
|
||||||
if (widgetColorDot == null) return;
|
|
||||||
|
|
||||||
ColorDot.applyColorDot(
|
|
||||||
widgetColorDot,
|
|
||||||
currentColor,
|
|
||||||
widgetColorDot.isEnabled()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDialogColorDot() {
|
|
||||||
if (dialogColorDot == null) return;
|
|
||||||
|
|
||||||
ColorDot.applyColorDot(
|
|
||||||
dialogColorDot,
|
|
||||||
currentColor,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -20,76 +21,59 @@ import androidx.annotation.ColorInt;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
|
* A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
|
||||||
* and an optional opacity slider.
|
|
||||||
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This view displays three main components for color selection:
|
* This view displays two main components for color selection:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
||||||
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the
|
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
|
||||||
* saturation and value (brightness) components of the color based on the selected hue.
|
* components of the color based on the selected hue.
|
||||||
* <li><b>Opacity Slider:</b> An optional horizontal bar below the hue bar that allows the user to adjust
|
|
||||||
* the opacity (alpha channel) of the color.
|
|
||||||
* </ul>
|
* </ul>
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar,
|
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
|
||||||
* opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
* saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
||||||
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
||||||
*/
|
*/
|
||||||
public class ColorPickerView extends View {
|
public class ColorPickerView extends View {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface definition for a callback to be invoked when the selected color changes.
|
* Interface definition for a callback to be invoked when the selected color changes.
|
||||||
*/
|
*/
|
||||||
public interface OnColorChangedListener {
|
public interface OnColorChangedListener {
|
||||||
/**
|
/**
|
||||||
* Called when the selected color has changed.
|
* Called when the selected color has changed.
|
||||||
|
*
|
||||||
|
* Important: Callback color uses RGB format with zero alpha channel.
|
||||||
|
*
|
||||||
|
* @param color The new selected color.
|
||||||
*/
|
*/
|
||||||
void onColorChanged(@ColorInt int color);
|
void onColorChanged(@ColorInt int color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
|
/** Expanded touch area for the hue bar to increase the touch-sensitive area. */
|
||||||
public static final float TOUCH_EXPANSION = Dim.dp20;
|
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
||||||
|
|
||||||
/** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
|
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||||
private static final float MARGIN_BETWEEN_AREAS = Dim.dp24;
|
private static final float VIEW_PADDING = dipToPixels(16);
|
||||||
|
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||||
/** Padding around the view. */
|
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||||
private static final float VIEW_PADDING = Dim.dp16;
|
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||||
|
|
||||||
/** Height of the hue bar. */
|
|
||||||
private static final float HUE_BAR_HEIGHT = Dim.dp12;
|
|
||||||
|
|
||||||
/** Height of the opacity slider. */
|
|
||||||
private static final float OPACITY_BAR_HEIGHT = Dim.dp12;
|
|
||||||
|
|
||||||
/** Corner radius for the hue bar. */
|
|
||||||
private static final float HUE_CORNER_RADIUS = Dim.dp6;
|
|
||||||
|
|
||||||
/** Corner radius for the opacity slider. */
|
|
||||||
private static final float OPACITY_CORNER_RADIUS = Dim.dp6;
|
|
||||||
|
|
||||||
/** Radius of the selector handles. */
|
|
||||||
private static final float SELECTOR_RADIUS = Dim.dp12;
|
|
||||||
|
|
||||||
/** Stroke width for the selector handle outlines. */
|
|
||||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill,
|
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
||||||
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||||
*/
|
*/
|
||||||
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
/** Thin dark outline stroke width for the selector rings. */
|
/** Thin dark outline stroke width for the selector rings. */
|
||||||
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
||||||
|
|
||||||
/** Radius for the outer edge of the selector rings, including stroke width. */
|
|
||||||
public static final float SELECTOR_EDGE_RADIUS =
|
public static final float SELECTOR_EDGE_RADIUS =
|
||||||
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
@@ -101,7 +85,6 @@ public class ColorPickerView extends View {
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
||||||
|
|
||||||
/** Precomputed array of hue colors for the hue bar (0-360 degrees). */
|
|
||||||
private static final int[] HUE_COLORS = new int[361];
|
private static final int[] HUE_COLORS = new int[361];
|
||||||
static {
|
static {
|
||||||
for (int i = 0; i < 361; i++) {
|
for (int i = 0; i < 361; i++) {
|
||||||
@@ -109,16 +92,11 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paint for the hue bar. */
|
/** Hue bar. */
|
||||||
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
/** Saturation-value selector. */
|
||||||
/** Paint for the opacity slider. */
|
|
||||||
private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
|
|
||||||
/** Paint for the saturation-value selector. */
|
|
||||||
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
/** Draggable selector. */
|
||||||
/** Paint for the draggable selector handles. */
|
|
||||||
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
{
|
{
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
@@ -126,10 +104,6 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/** Bounds of the hue bar. */
|
/** Bounds of the hue bar. */
|
||||||
private final RectF hueRect = new RectF();
|
private final RectF hueRect = new RectF();
|
||||||
|
|
||||||
/** Bounds of the opacity slider. */
|
|
||||||
private final RectF opacityRect = new RectF();
|
|
||||||
|
|
||||||
/** Bounds of the saturation-value selector. */
|
/** Bounds of the saturation-value selector. */
|
||||||
private final RectF saturationValueRect = new RectF();
|
private final RectF saturationValueRect = new RectF();
|
||||||
|
|
||||||
@@ -138,35 +112,21 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/** Current hue value (0-360). */
|
/** Current hue value (0-360). */
|
||||||
private float hue = 0f;
|
private float hue = 0f;
|
||||||
|
|
||||||
/** Current saturation value (0-1). */
|
/** Current saturation value (0-1). */
|
||||||
private float saturation = 1f;
|
private float saturation = 1f;
|
||||||
|
|
||||||
/** Current value (brightness) value (0-1). */
|
/** Current value (brightness) value (0-1). */
|
||||||
private float value = 1f;
|
private float value = 1f;
|
||||||
|
|
||||||
/** Current opacity value (0-1). */
|
/** The currently selected color in RGB format with no alpha channel. */
|
||||||
private float opacity = 1f;
|
|
||||||
|
|
||||||
/** The currently selected color, including alpha channel if opacity slider is enabled. */
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int selectedColor;
|
private int selectedColor;
|
||||||
|
|
||||||
/** Listener for color change events. */
|
|
||||||
private OnColorChangedListener colorChangedListener;
|
private OnColorChangedListener colorChangedListener;
|
||||||
|
|
||||||
/** Tracks if the hue selector is being dragged. */
|
/** Track if we're currently dragging the hue or saturation handle. */
|
||||||
private boolean isDraggingHue;
|
private boolean isDraggingHue;
|
||||||
|
|
||||||
/** Tracks if the saturation-value selector is being dragged. */
|
|
||||||
private boolean isDraggingSaturation;
|
private boolean isDraggingSaturation;
|
||||||
|
|
||||||
/** Tracks if the opacity selector is being dragged. */
|
|
||||||
private boolean isDraggingOpacity;
|
|
||||||
|
|
||||||
/** Flag to enable/disable the opacity slider. */
|
|
||||||
private boolean opacitySliderEnabled = false;
|
|
||||||
|
|
||||||
public ColorPickerView(Context context) {
|
public ColorPickerView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -179,32 +139,12 @@ public class ColorPickerView extends View {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables or disables the opacity slider.
|
|
||||||
*/
|
|
||||||
public void setOpacitySliderEnabled(boolean enabled) {
|
|
||||||
if (opacitySliderEnabled != enabled) {
|
|
||||||
opacitySliderEnabled = enabled;
|
|
||||||
if (!enabled) {
|
|
||||||
opacity = 1f; // Reset to fully opaque when disabled.
|
|
||||||
updateSelectedColor();
|
|
||||||
}
|
|
||||||
updateOpacityShader();
|
|
||||||
requestLayout(); // Trigger re-measure to account for opacity slider.
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the view, ensuring a consistent aspect ratio and minimum dimensions.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
final int minWidth = Dim.dp(250);
|
final int minWidth = Utils.dipToPixels(250);
|
||||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||||
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
|
||||||
|
|
||||||
int width = resolveSize(minWidth, widthMeasureSpec);
|
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||||
int height = resolveSize(minHeight, heightMeasureSpec);
|
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||||
@@ -214,8 +154,7 @@ public class ColorPickerView extends View {
|
|||||||
height = Math.max(height, minHeight);
|
height = Math.max(height, minHeight);
|
||||||
|
|
||||||
// Adjust height to maintain desired aspect ratio if possible.
|
// Adjust height to maintain desired aspect ratio if possible.
|
||||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||||
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
|
||||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||||
height = desiredHeight;
|
height = desiredHeight;
|
||||||
}
|
}
|
||||||
@@ -224,16 +163,17 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the view's layout when its size changes, recalculating bounds and shaders.
|
* Called when the size of the view changes.
|
||||||
|
* This method calculates and sets the bounds of the hue bar and saturation-value selector.
|
||||||
|
* It also creates the necessary shaders for the gradients.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||||
|
|
||||||
// Calculate bounds with hue bar and optional opacity bar at the bottom.
|
// Calculate bounds with hue bar at the bottom.
|
||||||
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||||
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS
|
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
|
||||||
- (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0);
|
|
||||||
|
|
||||||
// Adjust rectangles to account for padding and density-independent dimensions.
|
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||||
saturationValueRect.set(
|
saturationValueRect.set(
|
||||||
@@ -245,28 +185,18 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
hueRect.set(
|
hueRect.set(
|
||||||
VIEW_PADDING,
|
VIEW_PADDING,
|
||||||
height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0),
|
height - VIEW_PADDING - HUE_BAR_HEIGHT,
|
||||||
VIEW_PADDING + effectiveWidth,
|
VIEW_PADDING + effectiveWidth,
|
||||||
height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0)
|
height - VIEW_PADDING
|
||||||
);
|
);
|
||||||
|
|
||||||
if (opacitySliderEnabled) {
|
|
||||||
opacityRect.set(
|
|
||||||
VIEW_PADDING,
|
|
||||||
height - VIEW_PADDING - OPACITY_BAR_HEIGHT,
|
|
||||||
VIEW_PADDING + effectiveWidth,
|
|
||||||
height - VIEW_PADDING
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the shaders.
|
// Update the shaders.
|
||||||
updateHueShader();
|
updateHueShader();
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
updateOpacityShader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the shader for the hue bar to reflect the color gradient.
|
* Updates the hue full spectrum (0-360 degrees).
|
||||||
*/
|
*/
|
||||||
private void updateHueShader() {
|
private void updateHueShader() {
|
||||||
LinearGradient hueShader = new LinearGradient(
|
LinearGradient hueShader = new LinearGradient(
|
||||||
@@ -281,29 +211,8 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the shader for the opacity slider to reflect the current RGB color with varying opacity.
|
* Updates the shader for the saturation-value selector based on the currently selected hue.
|
||||||
*/
|
* This method creates a combined shader that blends a saturation gradient with a value gradient.
|
||||||
private void updateOpacityShader() {
|
|
||||||
if (!opacitySliderEnabled) {
|
|
||||||
opacityPaint.setShader(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a linear gradient for opacity from transparent to opaque, using the current RGB color.
|
|
||||||
int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
|
||||||
LinearGradient opacityShader = new LinearGradient(
|
|
||||||
opacityRect.left, opacityRect.top,
|
|
||||||
opacityRect.right, opacityRect.top,
|
|
||||||
rgbColor & 0x00FFFFFF, // Fully transparent
|
|
||||||
rgbColor | 0xFF000000, // Fully opaque
|
|
||||||
Shader.TileMode.CLAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
opacityPaint.setShader(opacityShader);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the shader for the saturation-value selector to reflect the current hue.
|
|
||||||
*/
|
*/
|
||||||
private void updateSaturationValueShader() {
|
private void updateSaturationValueShader() {
|
||||||
// Create a saturation-value gradient based on the current hue.
|
// Create a saturation-value gradient based on the current hue.
|
||||||
@@ -323,6 +232,7 @@ public class ColorPickerView extends View {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
||||||
|
//noinspection ExtractMethodRecommender
|
||||||
LinearGradient valShader = new LinearGradient(
|
LinearGradient valShader = new LinearGradient(
|
||||||
saturationValueRect.left, saturationValueRect.top,
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
saturationValueRect.left, saturationValueRect.bottom,
|
saturationValueRect.left, saturationValueRect.bottom,
|
||||||
@@ -339,7 +249,11 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles.
|
* Draws the color picker view on the canvas.
|
||||||
|
* This method draws the saturation-value selector, the hue bar with rounded corners,
|
||||||
|
* and the draggable handles.
|
||||||
|
*
|
||||||
|
* @param canvas The canvas on which to draw.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
@@ -349,67 +263,49 @@ public class ColorPickerView extends View {
|
|||||||
// Draw the hue bar.
|
// Draw the hue bar.
|
||||||
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||||
|
|
||||||
// Draw the opacity bar if enabled.
|
|
||||||
if (opacitySliderEnabled) {
|
|
||||||
canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
final float hueSelectorY = hueRect.centerY();
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
// Draw the saturation and hue selector handles filled with their respective colors (fully opaque).
|
// Draw the saturation and hue selector handle filled with the selected color.
|
||||||
hsvArray[0] = hue;
|
hsvArray[0] = hue;
|
||||||
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle.
|
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
|
||||||
final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle.
|
|
||||||
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
|
||||||
selectorPaint.setColor(hueHandleColor);
|
selectorPaint.setColor(hueHandleColor);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
selectorPaint.setColor(satHandleColor);
|
selectorPaint.setColor(selectedColor | 0xFF000000);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
if (opacitySliderEnabled) {
|
|
||||||
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
|
||||||
final float opacitySelectorY = opacityRect.centerY();
|
|
||||||
selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity.
|
|
||||||
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw white outlines for the handles.
|
// Draw white outlines for the handles.
|
||||||
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
||||||
selectorPaint.setStyle(Paint.Style.STROKE);
|
selectorPaint.setStyle(Paint.Style.STROKE);
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
if (opacitySliderEnabled) {
|
|
||||||
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
|
||||||
final float opacitySelectorY = opacityRect.centerY();
|
|
||||||
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
||||||
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
if (opacitySliderEnabled) {
|
|
||||||
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
|
||||||
final float opacitySelectorY = opacityRect.centerY();
|
|
||||||
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors.
|
* Handles touch events on the view.
|
||||||
|
* This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
|
||||||
|
* updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
|
||||||
|
* <p>
|
||||||
|
* In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
|
||||||
|
* is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
|
||||||
*
|
*
|
||||||
* @param event The motion event.
|
* @param event The motion event.
|
||||||
* @return True if the event was handled, false otherwise.
|
* @return True if the event was handled, false otherwise.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
try {
|
try {
|
||||||
@@ -418,19 +314,13 @@ public class ColorPickerView extends View {
|
|||||||
final int action = event.getAction();
|
final int action = event.getAction();
|
||||||
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
||||||
|
|
||||||
// Define touch expansion for the hue and opacity bars.
|
// Define touch expansion for the hue bar.
|
||||||
RectF expandedHueRect = new RectF(
|
RectF expandedHueRect = new RectF(
|
||||||
hueRect.left,
|
hueRect.left,
|
||||||
hueRect.top - TOUCH_EXPANSION,
|
hueRect.top - TOUCH_EXPANSION,
|
||||||
hueRect.right,
|
hueRect.right,
|
||||||
hueRect.bottom + TOUCH_EXPANSION
|
hueRect.bottom + TOUCH_EXPANSION
|
||||||
);
|
);
|
||||||
RectF expandedOpacityRect = opacitySliderEnabled ? new RectF(
|
|
||||||
opacityRect.left,
|
|
||||||
opacityRect.top - TOUCH_EXPANSION,
|
|
||||||
opacityRect.right,
|
|
||||||
opacityRect.bottom + TOUCH_EXPANSION
|
|
||||||
) : new RectF();
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
@@ -441,10 +331,7 @@ public class ColorPickerView extends View {
|
|||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0;
|
// Create hit areas for both handles.
|
||||||
final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0;
|
|
||||||
|
|
||||||
// Create hit areas for all handles.
|
|
||||||
RectF hueHitRect = new RectF(
|
RectF hueHitRect = new RectF(
|
||||||
hueSelectorX - SELECTOR_RADIUS,
|
hueSelectorX - SELECTOR_RADIUS,
|
||||||
hueSelectorY - SELECTOR_RADIUS,
|
hueSelectorY - SELECTOR_RADIUS,
|
||||||
@@ -457,23 +344,14 @@ public class ColorPickerView extends View {
|
|||||||
satSelectorX + SELECTOR_RADIUS,
|
satSelectorX + SELECTOR_RADIUS,
|
||||||
valSelectorY + SELECTOR_RADIUS
|
valSelectorY + SELECTOR_RADIUS
|
||||||
);
|
);
|
||||||
RectF opacityHitRect = opacitySliderEnabled ? new RectF(
|
|
||||||
opacitySelectorX - SELECTOR_RADIUS,
|
|
||||||
opacitySelectorY - SELECTOR_RADIUS,
|
|
||||||
opacitySelectorX + SELECTOR_RADIUS,
|
|
||||||
opacitySelectorY + SELECTOR_RADIUS
|
|
||||||
) : new RectF();
|
|
||||||
|
|
||||||
// Check if the touch started on a handle or within the expanded bar areas.
|
// Check if the touch started on a handle or within the expanded hue bar area.
|
||||||
if (hueHitRect.contains(x, y)) {
|
if (hueHitRect.contains(x, y)) {
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
updateHueFromTouch(x);
|
updateHueFromTouch(x);
|
||||||
} else if (satValHitRect.contains(x, y)) {
|
} else if (satValHitRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
} else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) {
|
|
||||||
isDraggingOpacity = true;
|
|
||||||
updateOpacityFromTouch(x);
|
|
||||||
} else if (expandedHueRect.contains(x, y)) {
|
} else if (expandedHueRect.contains(x, y)) {
|
||||||
// Handle touch within the expanded hue bar area.
|
// Handle touch within the expanded hue bar area.
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
@@ -481,9 +359,6 @@ public class ColorPickerView extends View {
|
|||||||
} else if (saturationValueRect.contains(x, y)) {
|
} else if (saturationValueRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
} else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) {
|
|
||||||
isDraggingOpacity = true;
|
|
||||||
updateOpacityFromTouch(x);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -493,8 +368,6 @@ public class ColorPickerView extends View {
|
|||||||
updateHueFromTouch(x);
|
updateHueFromTouch(x);
|
||||||
} else if (isDraggingSaturation) {
|
} else if (isDraggingSaturation) {
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
} else if (isDraggingOpacity) {
|
|
||||||
updateOpacityFromTouch(x);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -502,7 +375,6 @@ public class ColorPickerView extends View {
|
|||||||
case MotionEvent.ACTION_CANCEL:
|
case MotionEvent.ACTION_CANCEL:
|
||||||
isDraggingHue = false;
|
isDraggingHue = false;
|
||||||
isDraggingSaturation = false;
|
isDraggingSaturation = false;
|
||||||
isDraggingOpacity = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -513,7 +385,9 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the hue value based on a touch event.
|
* Updates the hue value based on touch position, clamping to valid range.
|
||||||
|
*
|
||||||
|
* @param x The x-coordinate of the touch position.
|
||||||
*/
|
*/
|
||||||
private void updateHueFromTouch(float x) {
|
private void updateHueFromTouch(float x) {
|
||||||
// Clamp x to the hue rectangle bounds.
|
// Clamp x to the hue rectangle bounds.
|
||||||
@@ -525,12 +399,14 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
hue = updatedHue;
|
hue = updatedHue;
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
updateOpacityShader();
|
|
||||||
updateSelectedColor();
|
updateSelectedColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the saturation and value based on a touch event.
|
* Updates saturation and value based on touch position, clamping to valid range.
|
||||||
|
*
|
||||||
|
* @param x The x-coordinate of the touch position.
|
||||||
|
* @param y The y-coordinate of the touch position.
|
||||||
*/
|
*/
|
||||||
private void updateSaturationValueFromTouch(float x, float y) {
|
private void updateSaturationValueFromTouch(float x, float y) {
|
||||||
// Clamp x and y to the saturation-value rectangle bounds.
|
// Clamp x and y to the saturation-value rectangle bounds.
|
||||||
@@ -545,34 +421,14 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
saturation = updatedSaturation;
|
saturation = updatedSaturation;
|
||||||
value = updatedValue;
|
value = updatedValue;
|
||||||
updateOpacityShader();
|
|
||||||
updateSelectedColor();
|
updateSelectedColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the opacity value based on a touch event.
|
* Updates the selected color and notifies listeners.
|
||||||
*/
|
|
||||||
private void updateOpacityFromTouch(float x) {
|
|
||||||
if (!opacitySliderEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right);
|
|
||||||
final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width();
|
|
||||||
if (opacity == updatedOpacity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
opacity = updatedOpacity;
|
|
||||||
updateSelectedColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the selected color based on the current hue, saturation, value, and opacity.
|
|
||||||
*/
|
*/
|
||||||
private void updateSelectedColor() {
|
private void updateSelectedColor() {
|
||||||
final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
final int updatedColor = opacitySliderEnabled
|
|
||||||
? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24)
|
|
||||||
: (rgbColor & 0x00FFFFFF) | 0xFF000000;
|
|
||||||
|
|
||||||
if (selectedColor != updatedColor) {
|
if (selectedColor != updatedColor) {
|
||||||
selectedColor = updatedColor;
|
selectedColor = updatedColor;
|
||||||
@@ -588,16 +444,19 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly.
|
* Sets the currently selected color.
|
||||||
|
*
|
||||||
|
* @param color The color to set in either ARGB or RGB format.
|
||||||
*/
|
*/
|
||||||
public void setColor(@ColorInt int color) {
|
public void setColor(@ColorInt int color) {
|
||||||
|
color &= 0x00FFFFFF;
|
||||||
if (selectedColor == color) {
|
if (selectedColor == color) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the selected color.
|
// Update the selected color.
|
||||||
selectedColor = color;
|
selectedColor = color;
|
||||||
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled));
|
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
|
||||||
|
|
||||||
// Convert the ARGB color to HSV values.
|
// Convert the ARGB color to HSV values.
|
||||||
float[] hsv = new float[3];
|
float[] hsv = new float[3];
|
||||||
@@ -607,11 +466,9 @@ public class ColorPickerView extends View {
|
|||||||
hue = hsv[0];
|
hue = hsv[0];
|
||||||
saturation = hsv[1];
|
saturation = hsv[1];
|
||||||
value = hsv[2];
|
value = hsv[2];
|
||||||
opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f;
|
|
||||||
|
|
||||||
// Update the saturation-value shader based on the new hue.
|
// Update the saturation-value shader based on the new hue.
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
updateOpacityShader();
|
|
||||||
|
|
||||||
// Notify the listener if it's set.
|
// Notify the listener if it's set.
|
||||||
if (colorChangedListener != null) {
|
if (colorChangedListener != null) {
|
||||||
@@ -624,6 +481,8 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the currently selected color.
|
* Gets the currently selected color.
|
||||||
|
*
|
||||||
|
* @return The selected color in RGB format with no alpha channel.
|
||||||
*/
|
*/
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public int getColor() {
|
public int getColor() {
|
||||||
@@ -631,7 +490,9 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a listener to be notified when the selected color changes.
|
* Sets the listener to be notified when the selected color changes.
|
||||||
|
*
|
||||||
|
* @param listener The listener to set.
|
||||||
*/
|
*/
|
||||||
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
||||||
colorChangedListener = listener;
|
colorChangedListener = listener;
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended ColorPickerPreference that enables the opacity slider for color selection.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference {
|
|
||||||
|
|
||||||
public ColorPickerWithOpacitySliderPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the preference with opacity slider enabled.
|
|
||||||
*/
|
|
||||||
private void init() {
|
|
||||||
// Enable the opacity slider for alpha channel support.
|
|
||||||
setOpacitySliderEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -11,86 +9,18 @@ import android.util.Pair;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.*;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator,
|
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
|
||||||
* supports a static summary and highlighted entries for search functionality.
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class CustomDialogListPreference extends ListPreference {
|
public class CustomDialogListPreference extends ListPreference {
|
||||||
|
|
||||||
public static final int ID_REVANCED_CHECK_ICON =
|
|
||||||
getResourceIdentifierOrThrow("revanced_check_icon", "id");
|
|
||||||
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
|
|
||||||
getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
|
|
||||||
public static final int ID_REVANCED_ITEM_TEXT =
|
|
||||||
getResourceIdentifierOrThrow("revanced_item_text", "id");
|
|
||||||
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
|
|
||||||
getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
|
|
||||||
|
|
||||||
private String staticSummary = null;
|
|
||||||
private CharSequence[] highlightedEntriesForDialog = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a static summary that will not be overwritten by value changes.
|
|
||||||
*/
|
|
||||||
public void setStaticSummary(String summary) {
|
|
||||||
this.staticSummary = summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the static summary if set, otherwise null.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public String getStaticSummary() {
|
|
||||||
return staticSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Always return static summary if set.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public CharSequence getSummary() {
|
|
||||||
if (staticSummary != null) {
|
|
||||||
return staticSummary;
|
|
||||||
}
|
|
||||||
return super.getSummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets highlighted entries for display in the dialog.
|
|
||||||
* These entries are used only for the current dialog and are automatically cleared.
|
|
||||||
*/
|
|
||||||
public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) {
|
|
||||||
this.highlightedEntriesForDialog = highlightedEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears highlighted entries after the dialog is closed.
|
|
||||||
*/
|
|
||||||
public void clearHighlightedEntriesForDialog() {
|
|
||||||
this.highlightedEntriesForDialog = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns entries for display in the dialog.
|
|
||||||
* If highlighted entries exist, they are used; otherwise, the original entries are returned.
|
|
||||||
*/
|
|
||||||
private CharSequence[] getEntriesForDialog() {
|
|
||||||
return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom ArrayAdapter to handle checkmark visibility.
|
* Custom ArrayAdapter to handle checkmark visibility.
|
||||||
*/
|
*/
|
||||||
@@ -105,10 +35,8 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
final CharSequence[] entryValues;
|
final CharSequence[] entryValues;
|
||||||
String selectedValue;
|
String selectedValue;
|
||||||
|
|
||||||
public ListPreferenceArrayAdapter(Context context, int resource,
|
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
|
||||||
CharSequence[] entries,
|
CharSequence[] entryValues, String selectedValue) {
|
||||||
CharSequence[] entryValues,
|
|
||||||
String selectedValue) {
|
|
||||||
super(context, resource, entries);
|
super(context, resource, entries);
|
||||||
this.layoutResourceId = resource;
|
this.layoutResourceId = resource;
|
||||||
this.entryValues = entryValues;
|
this.entryValues = entryValues;
|
||||||
@@ -125,16 +53,19 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||||
view = inflater.inflate(layoutResourceId, parent, false);
|
view = inflater.inflate(layoutResourceId, parent, false);
|
||||||
holder = new SubViewDataContainer();
|
holder = new SubViewDataContainer();
|
||||||
holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
|
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
|
||||||
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
"revanced_check_icon", "id"));
|
||||||
holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
|
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
|
||||||
|
"revanced_check_icon_placeholder", "id"));
|
||||||
|
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
|
||||||
|
"revanced_item_text", "id"));
|
||||||
view.setTag(holder);
|
view.setTag(holder);
|
||||||
} else {
|
} else {
|
||||||
holder = (SubViewDataContainer) view.getTag();
|
holder = (SubViewDataContainer) view.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
CharSequence itemText = getItem(position);
|
// Set text.
|
||||||
holder.itemText.setText(itemText);
|
holder.itemText.setText(getItem(position));
|
||||||
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
|
||||||
// Show or hide checkmark and placeholder.
|
// Show or hide checkmark and placeholder.
|
||||||
@@ -172,9 +103,6 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
CharSequence[] entriesToShow = getEntriesForDialog();
|
|
||||||
CharSequence[] entryValues = getEntryValues();
|
|
||||||
|
|
||||||
// Create ListView.
|
// Create ListView.
|
||||||
ListView listView = new ListView(context);
|
ListView listView = new ListView(context);
|
||||||
listView.setId(android.R.id.list);
|
listView.setId(android.R.id.list);
|
||||||
@@ -183,9 +111,9 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
// Create custom adapter for the ListView.
|
// Create custom adapter for the ListView.
|
||||||
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
||||||
context,
|
context,
|
||||||
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
|
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
||||||
entriesToShow,
|
getEntries(),
|
||||||
entryValues,
|
getEntryValues(),
|
||||||
getValue()
|
getValue()
|
||||||
);
|
);
|
||||||
listView.setAdapter(adapter);
|
listView.setAdapter(adapter);
|
||||||
@@ -193,6 +121,7 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
// Set checked item.
|
// Set checked item.
|
||||||
String currentValue = getValue();
|
String currentValue = getValue();
|
||||||
if (currentValue != null) {
|
if (currentValue != null) {
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
for (int i = 0, length = entryValues.length; i < length; i++) {
|
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||||
if (currentValue.equals(entryValues[i].toString())) {
|
if (currentValue.equals(entryValues[i].toString())) {
|
||||||
listView.setItemChecked(i, true);
|
listView.setItemChecked(i, true);
|
||||||
@@ -203,23 +132,19 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the custom dialog without OK button.
|
// Create the custom dialog without OK button.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "",
|
getTitle() != null ? getTitle().toString() : "",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null, // No OK button text.
|
||||||
null,
|
null, // No OK button action.
|
||||||
this::clearHighlightedEntriesForDialog, // Cancel button action.
|
() -> {}, // Cancel button action (just dismiss).
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
|
||||||
// Add a listener to clear when the dialog is closed in any way.
|
|
||||||
dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog());
|
|
||||||
|
|
||||||
// Add the ListView to the main layout.
|
// Add the ListView to the main layout.
|
||||||
LinearLayout mainLayout = dialogPair.second;
|
LinearLayout mainLayout = dialogPair.second;
|
||||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||||
@@ -231,28 +156,16 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
|
|
||||||
// Handle item click to select value and dismiss dialog.
|
// Handle item click to select value and dismiss dialog.
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
String selectedValue = entryValues[position].toString();
|
String selectedValue = getEntryValues()[position].toString();
|
||||||
if (callChangeListener(selectedValue)) {
|
if (callChangeListener(selectedValue)) {
|
||||||
setValue(selectedValue);
|
setValue(selectedValue);
|
||||||
|
|
||||||
// Update summaries from the original entries (without highlighting).
|
|
||||||
if (staticSummary == null) {
|
|
||||||
CharSequence[] originalEntries = getEntries();
|
|
||||||
if (originalEntries != null && position < originalEntries.length) {
|
|
||||||
setSummary(originalEntries[position]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.setSelectedValue(selectedValue);
|
adapter.setSelectedValue(selectedValue);
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
dialogPair.first.dismiss();
|
||||||
// Clear highlighted entries before closing.
|
|
||||||
clearHighlightedEntriesForDialog();
|
|
||||||
dialog.dismiss();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
dialog.show();
|
dialogPair.first.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,623 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.util.SparseBooleanArray;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Space;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom preference that opens a dialog for managing feature flags.
|
|
||||||
* Allows moving boolean flags between active and blocked states with advanced selection.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
|
||||||
public class FeatureFlagsManagerPreference extends Preference {
|
|
||||||
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable");
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flags to hide from the UI.
|
|
||||||
*/
|
|
||||||
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
|
|
||||||
45386834L // 'You' tab settings icon.
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks state for range selection in ListView.
|
|
||||||
*/
|
|
||||||
private static class ListViewSelectionState {
|
|
||||||
int lastClickedPosition = -1; // Position of the last clicked item.
|
|
||||||
boolean isRangeSelecting = false; // True while a range is being selected.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to pass ListView and Adapter together.
|
|
||||||
*/
|
|
||||||
private record ColumnViews(ListView listView, FlagAdapter adapter) {}
|
|
||||||
|
|
||||||
{
|
|
||||||
setOnPreferenceClickListener(pref -> {
|
|
||||||
showFlagsManagerDialog();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeatureFlagsManagerPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FeatureFlagsManagerPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the main dialog for managing feature flags.
|
|
||||||
*/
|
|
||||||
private void showFlagsManagerDialog() {
|
|
||||||
if (!BaseSettings.DEBUG.get()) {
|
|
||||||
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = getContext();
|
|
||||||
|
|
||||||
// Load all known and disabled flags.
|
|
||||||
TreeSet<Long> allKnownFlags = new TreeSet<>(EnableDebuggingPatch.getAllLoggedFlags());
|
|
||||||
allKnownFlags.removeAll(FLAGS_TO_IGNORE);
|
|
||||||
|
|
||||||
TreeSet<Long> disabledFlags = new TreeSet<>(EnableDebuggingPatch.parseFlags(
|
|
||||||
BaseSettings.DISABLED_FEATURE_FLAGS.get()));
|
|
||||||
disabledFlags.removeAll(FLAGS_TO_IGNORE);
|
|
||||||
|
|
||||||
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
|
|
||||||
// String does not need to be localized because it's basically impossible
|
|
||||||
// to reach the settings menu without encountering at least 1 flag.
|
|
||||||
Utils.showToastShort("No feature flags logged yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeSet<Long> availableFlags = new TreeSet<>(allKnownFlags);
|
|
||||||
availableFlags.removeAll(disabledFlags);
|
|
||||||
TreeSet<Long> blockedFlags = new TreeSet<>(disabledFlags);
|
|
||||||
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
|
||||||
context,
|
|
||||||
getTitle() != null ? getTitle().toString() : "",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
str("revanced_settings_save"),
|
|
||||||
() -> saveFlags(blockedFlags),
|
|
||||||
() -> {},
|
|
||||||
str("revanced_settings_reset"),
|
|
||||||
this::resetFlags,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
LinearLayout mainLayout = dialogPair.second;
|
|
||||||
LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f);
|
|
||||||
|
|
||||||
// Insert content before the dialog button row.
|
|
||||||
View contentView = createContentView(context, availableFlags, blockedFlags);
|
|
||||||
mainLayout.addView(contentView, mainLayout.getChildCount() - 1, contentParams);
|
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window != null) {
|
|
||||||
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 100, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the main content view with two columns.
|
|
||||||
*/
|
|
||||||
private View createContentView(Context context, TreeSet<Long> availableFlags, TreeSet<Long> blockedFlags) {
|
|
||||||
LinearLayout contentLayout = new LinearLayout(context);
|
|
||||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
|
|
||||||
// Headers.
|
|
||||||
TextView availableHeader = createHeader(context, "revanced_debug_feature_flags_manager_active_header");
|
|
||||||
TextView blockedHeader = createHeader(context, "revanced_debug_feature_flags_manager_blocked_header");
|
|
||||||
|
|
||||||
LinearLayout headersLayout = new LinearLayout(context);
|
|
||||||
headersLayout.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
headersLayout.addView(availableHeader, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
|
||||||
headersLayout.addView(blockedHeader, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
|
||||||
|
|
||||||
// Columns.
|
|
||||||
View leftColumn = createColumn(context, availableFlags, availableHeader);
|
|
||||||
View rightColumn = createColumn(context, blockedFlags, blockedHeader);
|
|
||||||
|
|
||||||
ColumnViews leftViews = (ColumnViews) leftColumn.getTag();
|
|
||||||
ColumnViews rightViews = (ColumnViews) rightColumn.getTag();
|
|
||||||
|
|
||||||
updateHeaderCount(availableHeader, leftViews.adapter);
|
|
||||||
updateHeaderCount(blockedHeader, rightViews.adapter);
|
|
||||||
|
|
||||||
// Main columns layout.
|
|
||||||
LinearLayout columnsLayout = new LinearLayout(context);
|
|
||||||
columnsLayout.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
columnsLayout.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
|
||||||
columnsLayout.addView(leftColumn, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
|
||||||
|
|
||||||
Space spaceBetweenColumns = new Space(context);
|
|
||||||
spaceBetweenColumns.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
||||||
columnsLayout.addView(spaceBetweenColumns);
|
|
||||||
|
|
||||||
columnsLayout.addView(rightColumn, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
|
||||||
|
|
||||||
// Move buttons below columns.
|
|
||||||
Pair<LinearLayout, LinearLayout> moveButtons = createMoveButtons(context,
|
|
||||||
leftViews.listView, rightViews.listView,
|
|
||||||
availableFlags, blockedFlags, availableHeader, blockedHeader);
|
|
||||||
|
|
||||||
// Layout for buttons row.
|
|
||||||
LinearLayout buttonsRow = new LinearLayout(context);
|
|
||||||
buttonsRow.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
buttonsRow.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
buttonsRow.addView(moveButtons.first, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
|
||||||
|
|
||||||
Space spaceBetweenButtons = new Space(context);
|
|
||||||
spaceBetweenButtons.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
buttonsRow.addView(spaceBetweenButtons);
|
|
||||||
|
|
||||||
buttonsRow.addView(moveButtons.second, new LinearLayout.LayoutParams(
|
|
||||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
|
||||||
|
|
||||||
contentLayout.addView(headersLayout);
|
|
||||||
contentLayout.addView(columnsLayout);
|
|
||||||
contentLayout.addView(buttonsRow);
|
|
||||||
|
|
||||||
return contentLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a header TextView.
|
|
||||||
*/
|
|
||||||
private TextView createHeader(Context context, String tag) {
|
|
||||||
TextView textview = new TextView(context);
|
|
||||||
textview.setTag(tag);
|
|
||||||
textview.setTextSize(16);
|
|
||||||
textview.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
textview.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
return textview;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a single column (search + buttons + list).
|
|
||||||
*/
|
|
||||||
private View createColumn(Context context, TreeSet<Long> flags, TextView countText) {
|
|
||||||
LinearLayout wrapper = new LinearLayout(context);
|
|
||||||
wrapper.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
|
|
||||||
Pair<ListView, FlagAdapter> pair = createListView(context, flags, countText);
|
|
||||||
ListView listView = pair.first;
|
|
||||||
FlagAdapter adapter = pair.second;
|
|
||||||
|
|
||||||
EditText search = createSearchBox(context, adapter, listView, countText);
|
|
||||||
LinearLayout buttons = createActionButtons(context, listView, adapter);
|
|
||||||
|
|
||||||
listView.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(10), null, null));
|
|
||||||
background.getPaint().setColor(Utils.getEditTextBackground());
|
|
||||||
listView.setPadding(0, Dim.dp4, 0, Dim.dp4);
|
|
||||||
listView.setBackground(background);
|
|
||||||
listView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
|
||||||
|
|
||||||
wrapper.addView(search);
|
|
||||||
wrapper.addView(buttons);
|
|
||||||
wrapper.addView(listView);
|
|
||||||
|
|
||||||
// Save references for move buttons.
|
|
||||||
wrapper.setTag(new ColumnViews(listView, adapter));
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the header text with the current count.
|
|
||||||
*/
|
|
||||||
private void updateHeaderCount(TextView header, FlagAdapter adapter) {
|
|
||||||
header.setText(str((String) header.getTag(), adapter.getCount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a search box that filters the list.
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private EditText createSearchBox(Context context, FlagAdapter adapter, ListView listView, TextView countText) {
|
|
||||||
EditText search = new EditText(context);
|
|
||||||
search.setInputType(InputType.TYPE_CLASS_NUMBER);
|
|
||||||
search.setTextSize(16);
|
|
||||||
search.setHint(str("revanced_debug_feature_flags_manager_search_hint"));
|
|
||||||
search.setHapticFeedbackEnabled(false);
|
|
||||||
search.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
search.addTextChangedListener(new TextWatcher() {
|
|
||||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
||||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
adapter.setSearchQuery(s.toString());
|
|
||||||
listView.clearChoices();
|
|
||||||
updateHeaderCount(countText, adapter);
|
|
||||||
Drawable clearIcon = context.getResources().getDrawable(android.R.drawable.ic_menu_close_clear_cancel);
|
|
||||||
clearIcon.setBounds(0, 0, Dim.dp20, Dim.dp20);
|
|
||||||
search.setCompoundDrawables(null, null, TextUtils.isEmpty(s) ? null : clearIcon, null);
|
|
||||||
}
|
|
||||||
@Override public void afterTextChanged(Editable s) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
search.setOnTouchListener((v, event) -> {
|
|
||||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
|
||||||
Drawable[] compoundDrawables = search.getCompoundDrawables();
|
|
||||||
if (compoundDrawables[2] != null &&
|
|
||||||
event.getRawX() >= (search.getRight() - compoundDrawables[2].getBounds().width())) {
|
|
||||||
search.setText("");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return search;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates action buttons.
|
|
||||||
*/
|
|
||||||
private LinearLayout createActionButtons(Context context, ListView listView, FlagAdapter adapter) {
|
|
||||||
LinearLayout row = new LinearLayout(context);
|
|
||||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
row.setGravity(Gravity.CENTER);
|
|
||||||
row.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
ImageButton selectAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_SELECT_ALL,
|
|
||||||
() -> {
|
|
||||||
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
|
||||||
listView.setItemChecked(i, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ImageButton clearAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL,
|
|
||||||
() -> {
|
|
||||||
listView.clearChoices();
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
});
|
|
||||||
|
|
||||||
ImageButton copy = createButton(context, DRAWABLE_REVANCED_SETTINGS_COPY_ALL,
|
|
||||||
() -> {
|
|
||||||
List<String> items = new ArrayList<>();
|
|
||||||
SparseBooleanArray checked = listView.getCheckedItemPositions();
|
|
||||||
|
|
||||||
if (checked.size() > 0) {
|
|
||||||
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
|
||||||
if (checked.get(i)) {
|
|
||||||
items.add(adapter.getItem(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (Long flag : adapter.getFullFlags()) {
|
|
||||||
items.add(String.valueOf(flag));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setClipboard(TextUtils.join("\n", items));
|
|
||||||
|
|
||||||
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_copied"));
|
|
||||||
});
|
|
||||||
|
|
||||||
row.addView(selectAll);
|
|
||||||
row.addView(clearAll);
|
|
||||||
row.addView(copy);
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the move buttons (left and right groups).
|
|
||||||
*/
|
|
||||||
private Pair<LinearLayout, LinearLayout> createMoveButtons(Context context,
|
|
||||||
ListView availableListView, ListView blockedListView,
|
|
||||||
TreeSet<Long> availableFlags, TreeSet<Long> blockedFlags,
|
|
||||||
TextView availableCountText, TextView blockedCountText) {
|
|
||||||
// Left group: >> >
|
|
||||||
LinearLayout leftButtons = new LinearLayout(context);
|
|
||||||
leftButtons.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
leftButtons.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
ImageButton moveAllRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE,
|
|
||||||
() -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags,
|
|
||||||
availableCountText, blockedCountText, true));
|
|
||||||
|
|
||||||
ImageButton moveOneRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE,
|
|
||||||
() -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags,
|
|
||||||
availableCountText, blockedCountText, false));
|
|
||||||
|
|
||||||
leftButtons.addView(moveAllRight);
|
|
||||||
leftButtons.addView(moveOneRight);
|
|
||||||
|
|
||||||
// Right group: < <<
|
|
||||||
LinearLayout rightButtons = new LinearLayout(context);
|
|
||||||
rightButtons.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
rightButtons.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
ImageButton moveOneLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE,
|
|
||||||
() -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags,
|
|
||||||
blockedCountText, availableCountText, false));
|
|
||||||
|
|
||||||
ImageButton moveAllLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE,
|
|
||||||
() -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags,
|
|
||||||
blockedCountText, availableCountText, true));
|
|
||||||
|
|
||||||
rightButtons.addView(moveOneLeft);
|
|
||||||
rightButtons.addView(moveAllLeft);
|
|
||||||
|
|
||||||
return new Pair<>(leftButtons, rightButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a styled ImageButton.
|
|
||||||
*/
|
|
||||||
@SuppressLint("ResourceType")
|
|
||||||
private ImageButton createButton(Context context, int drawableResId, Runnable action) {
|
|
||||||
ImageButton button = new ImageButton(context);
|
|
||||||
|
|
||||||
button.setImageResource(drawableResId);
|
|
||||||
button.setScaleType(ImageView.ScaleType.CENTER);
|
|
||||||
int[] attrs = {android.R.attr.selectableItemBackgroundBorderless};
|
|
||||||
//noinspection Recycle
|
|
||||||
TypedArray ripple = context.obtainStyledAttributes(attrs);
|
|
||||||
button.setBackgroundDrawable(ripple.getDrawable(0));
|
|
||||||
ripple.close();
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Dim.dp32, Dim.dp32);
|
|
||||||
params.setMargins(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8);
|
|
||||||
button.setLayoutParams(params);
|
|
||||||
|
|
||||||
button.setOnClickListener(v -> action.run());
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom adapter with search filtering.
|
|
||||||
*/
|
|
||||||
private static class FlagAdapter extends ArrayAdapter<String> {
|
|
||||||
private final TreeSet<Long> fullFlags;
|
|
||||||
private String searchQuery = "";
|
|
||||||
|
|
||||||
public FlagAdapter(Context context, TreeSet<Long> fullFlags) {
|
|
||||||
super(context, android.R.layout.simple_list_item_multiple_choice, new ArrayList<>());
|
|
||||||
this.fullFlags = fullFlags;
|
|
||||||
updateFiltered();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSearchQuery(String query) {
|
|
||||||
searchQuery = query == null ? "" : query.trim();
|
|
||||||
updateFiltered();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFiltered() {
|
|
||||||
clear();
|
|
||||||
for (Long flag : fullFlags) {
|
|
||||||
String flagString = String.valueOf(flag);
|
|
||||||
if (searchQuery.isEmpty() || flagString.contains(searchQuery)) {
|
|
||||||
add(flagString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refresh() {
|
|
||||||
updateFiltered();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Long> getFullFlags() {
|
|
||||||
return new ArrayList<>(fullFlags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a ListView with filtering, multi-select, and range selection.
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private Pair<ListView, FlagAdapter> createListView(Context context,
|
|
||||||
TreeSet<Long> flags, TextView countText) {
|
|
||||||
ListView listView = new ListView(context);
|
|
||||||
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
|
||||||
listView.setDividerHeight(0);
|
|
||||||
|
|
||||||
FlagAdapter adapter = new FlagAdapter(context, flags);
|
|
||||||
listView.setAdapter(adapter);
|
|
||||||
|
|
||||||
final ListViewSelectionState state = new ListViewSelectionState();
|
|
||||||
|
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
|
||||||
if (!state.isRangeSelecting) {
|
|
||||||
state.lastClickedPosition = position;
|
|
||||||
} else {
|
|
||||||
state.isRangeSelecting = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listView.setOnItemLongClickListener((parent, view, position, id) -> {
|
|
||||||
if (state.lastClickedPosition == -1) {
|
|
||||||
listView.setItemChecked(position, true);
|
|
||||||
state.lastClickedPosition = position;
|
|
||||||
} else {
|
|
||||||
int start = Math.min(state.lastClickedPosition, position);
|
|
||||||
int end = Math.max(state.lastClickedPosition, position);
|
|
||||||
for (int i = start; i <= end; i++) {
|
|
||||||
listView.setItemChecked(i, true);
|
|
||||||
}
|
|
||||||
state.isRangeSelecting = true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
listView.setOnTouchListener((view, event) -> {
|
|
||||||
if (event.getAction() == MotionEvent.ACTION_UP && state.isRangeSelecting) {
|
|
||||||
state.isRangeSelecting = false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Pair<>(listView, adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves selected or all flags from one list to another.
|
|
||||||
*
|
|
||||||
* @param fromListView Source ListView.
|
|
||||||
* @param toListView Destination ListView.
|
|
||||||
* @param fromFlags Source flag set.
|
|
||||||
* @param toFlags Destination flag set.
|
|
||||||
* @param fromCountText Header showing count of source items.
|
|
||||||
* @param toCountText Header showing count of destination items.
|
|
||||||
* @param moveAll If true, move all items; if false, move only selected.
|
|
||||||
*/
|
|
||||||
private void moveFlags(ListView fromListView, ListView toListView,
|
|
||||||
TreeSet<Long> fromFlags, TreeSet<Long> toFlags,
|
|
||||||
TextView fromCountText, TextView toCountText,
|
|
||||||
boolean moveAll) {
|
|
||||||
if (fromListView == null || toListView == null) return;
|
|
||||||
|
|
||||||
List<Long> flagsToMove = new ArrayList<>();
|
|
||||||
FlagAdapter fromAdapter = (FlagAdapter) fromListView.getAdapter();
|
|
||||||
|
|
||||||
if (moveAll) {
|
|
||||||
flagsToMove.addAll(fromFlags);
|
|
||||||
} else {
|
|
||||||
SparseBooleanArray checked = fromListView.getCheckedItemPositions();
|
|
||||||
for (int i = 0, count = fromAdapter.getCount(); i < count; i++) {
|
|
||||||
if (checked.get(i)) {
|
|
||||||
String item = fromAdapter.getItem(i);
|
|
||||||
if (item != null) {
|
|
||||||
flagsToMove.add(Long.parseLong(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flagsToMove.isEmpty()) return;
|
|
||||||
|
|
||||||
for (Long flag : flagsToMove) {
|
|
||||||
fromFlags.remove(flag);
|
|
||||||
toFlags.add(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear selections before refreshing.
|
|
||||||
fromListView.clearChoices();
|
|
||||||
toListView.clearChoices();
|
|
||||||
|
|
||||||
// Refresh both adapters.
|
|
||||||
fromAdapter.refresh();
|
|
||||||
((FlagAdapter) toListView.getAdapter()).refresh();
|
|
||||||
|
|
||||||
// Update headers.
|
|
||||||
updateHeaderCount(fromCountText, fromAdapter);
|
|
||||||
updateHeaderCount(toCountText, (FlagAdapter) toListView.getAdapter());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves blocked flags to settings.
|
|
||||||
*/
|
|
||||||
private void saveFlags(TreeSet<Long> blockedFlags) {
|
|
||||||
StringBuilder flagsString = new StringBuilder();
|
|
||||||
for (Long flag : blockedFlags) {
|
|
||||||
if (flagsString.length() > 0) {
|
|
||||||
flagsString.append("\n");
|
|
||||||
}
|
|
||||||
flagsString.append(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseSettings.DISABLED_FEATURE_FLAGS.save(flagsString.toString());
|
|
||||||
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_saved"));
|
|
||||||
Logger.printDebug(() -> "Feature flags saved. Blocked: " + blockedFlags.size());
|
|
||||||
|
|
||||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets all blocked flags.
|
|
||||||
*/
|
|
||||||
private void resetFlags() {
|
|
||||||
BaseSettings.DISABLED_FEATURE_FLAGS.save("");
|
|
||||||
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_reset"));
|
|
||||||
|
|
||||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
|
||||||
public class ForceOriginalAudioSwitchPreference extends SwitchPreference {
|
|
||||||
|
|
||||||
// Spoof stream patch is not included, or is not currently spoofing to Android Studio.
|
|
||||||
private static final boolean available = !SpoofVideoStreamsPatch.isPatchIncluded()
|
|
||||||
|| !(BaseSettings.SPOOF_VIDEO_STREAMS.get()
|
|
||||||
&& SpoofVideoStreamsPatch.getPreferredClient() == ClientType.ANDROID_CREATOR);
|
|
||||||
|
|
||||||
{
|
|
||||||
if (!available) {
|
|
||||||
// Show why force audio is not available.
|
|
||||||
String summary = str("revanced_force_original_audio_not_available");
|
|
||||||
super.setSummary(summary);
|
|
||||||
super.setSummaryOn(summary);
|
|
||||||
super.setSummaryOff(summary);
|
|
||||||
super.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
public ForceOriginalAudioSwitchPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
if (!available) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
if (!available) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setSummary(summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -9,16 +10,21 @@ import android.os.Bundle;
|
|||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||||
@@ -34,7 +40,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
editText.setAutofillHints((String) null);
|
editText.setAutofillHints((String) null);
|
||||||
}
|
}
|
||||||
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
editText.setTextSize(14);
|
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
|
||||||
|
|
||||||
setOnPreferenceClickListener(this);
|
setOnPreferenceClickListener(this);
|
||||||
}
|
}
|
||||||
@@ -76,7 +82,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
|
|
||||||
// Create a custom dialog with the EditText.
|
// Create a custom dialog with the EditText.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
context,
|
context,
|
||||||
str("revanced_pref_import_export_title"), // Title.
|
str("revanced_pref_import_export_title"), // Title.
|
||||||
null, // No message (EditText replaces it).
|
null, // No message (EditText replaces it).
|
||||||
@@ -92,20 +98,6 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
true // Dismiss dialog when onNeutralClick.
|
true // Dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there are no settings yet, then show the on screen keyboard and bring focus to
|
|
||||||
// the edit text. This makes it easier to paste saved settings after a reinstall.
|
|
||||||
dialogPair.first.setOnShowListener(dialogInterface -> {
|
|
||||||
if (existingSettings.isEmpty()) {
|
|
||||||
editText.postDelayed(() -> {
|
|
||||||
editText.requestFocus();
|
|
||||||
|
|
||||||
InputMethodManager inputMethodManager = (InputMethodManager)
|
|
||||||
editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
dialogPair.first.show();
|
dialogPair.first.show();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -17,7 +17,6 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
@@ -40,7 +39,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a dialog showing official links.
|
* Opens a dialog showing official links.
|
||||||
@@ -126,8 +124,6 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
|
|
||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
setOnPreferenceClickListener(pref -> {
|
||||||
Context context = pref.getContext();
|
|
||||||
|
|
||||||
// Show a progress spinner if the social links are not fetched yet.
|
// Show a progress spinner if the social links are not fetched yet.
|
||||||
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
||||||
// Show a progress spinner, but only if the api fetch takes more than a half a second.
|
// Show a progress spinner, but only if the api fetch takes more than a half a second.
|
||||||
@@ -140,18 +136,17 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
|
||||||
|
|
||||||
Utils.runOnBackgroundThread(() ->
|
Utils.runOnBackgroundThread(() ->
|
||||||
fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress));
|
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
|
||||||
} else {
|
} else {
|
||||||
// No network call required and can run now.
|
// No network call required and can run now.
|
||||||
fetchLinksAndShowDialog(context, null, null, null);
|
fetchLinksAndShowDialog(null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchLinksAndShowDialog(Context context,
|
private void fetchLinksAndShowDialog(@Nullable Handler handler,
|
||||||
@Nullable Handler handler,
|
|
||||||
Runnable showDialogRunnable,
|
Runnable showDialogRunnable,
|
||||||
@Nullable ProgressDialog progress) {
|
@Nullable ProgressDialog progress) {
|
||||||
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
|
||||||
@@ -168,17 +163,7 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.removeCallbacks(showDialogRunnable);
|
handler.removeCallbacks(showDialogRunnable);
|
||||||
}
|
}
|
||||||
|
if (progress != null) {
|
||||||
// Don't continue if the activity is done. To test this tap the
|
|
||||||
// about dialog and immediately press back before the dialog can show.
|
|
||||||
if (context instanceof Activity activity) {
|
|
||||||
if (activity.isFinishing() || activity.isDestroyed()) {
|
|
||||||
Logger.printDebug(() -> "Not showing about dialog, activity is closed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress != null && progress.isShowing()) {
|
|
||||||
progress.dismiss();
|
progress.dismiss();
|
||||||
}
|
}
|
||||||
new WebViewDialog(getContext(), htmlDialog).show();
|
new WebViewDialog(getContext(), htmlDialog).show();
|
||||||
@@ -222,10 +207,11 @@ class WebViewDialog extends Dialog {
|
|||||||
LinearLayout mainLayout = new LinearLayout(getContext());
|
LinearLayout mainLayout = new LinearLayout(getContext());
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
mainLayout.setPadding(Dim.dp10, Dim.dp10, Dim.dp10, Dim.dp10);
|
final int padding = dipToPixels(10);
|
||||||
|
mainLayout.setPadding(padding, padding, padding, padding);
|
||||||
// Set rounded rectangle background.
|
// Set rounded rectangle background.
|
||||||
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
Dim.roundedCorners(28), null, null));
|
Utils.createCornerRadii(28), null, null));
|
||||||
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||||
mainLayout.setBackground(mainBackground);
|
mainLayout.setBackground(mainBackground);
|
||||||
|
|
||||||
@@ -242,10 +228,10 @@ class WebViewDialog extends Dialog {
|
|||||||
|
|
||||||
setContentView(mainLayout);
|
setContentView(mainLayout);
|
||||||
|
|
||||||
// Set dialog window attributes.
|
// Set dialog window attributes
|
||||||
Window window = getWindow();
|
Window window = getWindow();
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
|
Utils.setDialogWindowParameters(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import androidx.annotation.Nullable;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ResettableEditTextPreference extends EditTextPreference {
|
public class ResettableEditTextPreference extends EditTextPreference {
|
||||||
@@ -66,7 +66,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
|
|
||||||
// Create custom dialog.
|
// Create custom dialog.
|
||||||
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||||
null, // Message is replaced by EditText.
|
null, // Message is replaced by EditText.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.graphics.Insets;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@@ -19,28 +19,9 @@ import androidx.annotation.Nullable;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the list of preferences from this fragment, if they exist.
|
|
||||||
* @param keys Preference keys.
|
|
||||||
*/
|
|
||||||
protected void removePreferences(String ... keys) {
|
|
||||||
for (String key : keys) {
|
|
||||||
Preference pref = findPreference(key);
|
|
||||||
if (pref != null) {
|
|
||||||
PreferenceGroup parent = pref.getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
Logger.printDebug(() -> "Removing preference: " + key);
|
|
||||||
parent.removePreference(pref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets toolbar for all nested preference screens.
|
* Sets toolbar for all nested preference screens.
|
||||||
*/
|
*/
|
||||||
@@ -88,13 +69,14 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
toolbar.setNavigationIcon(getBackButtonDrawable());
|
toolbar.setNavigationIcon(getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
||||||
|
|
||||||
toolbar.setTitleMargin(Dim.dp16, 0, Dim.dp16, 0);
|
final int margin = Utils.dipToPixels(16);
|
||||||
|
toolbar.setTitleMargin(margin, 0, margin, 0);
|
||||||
|
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar,
|
TextView toolbarTextView = Utils.getChildView(toolbar,
|
||||||
true, TextView.class::isInstance);
|
true, TextView.class::isInstance);
|
||||||
if (toolbarTextView != null) {
|
if (toolbarTextView != null) {
|
||||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||||
toolbarTextView.setTextSize(20);
|
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow package-specific toolbar customization.
|
// Allow package-specific toolbar customization.
|
||||||
@@ -133,7 +115,7 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
public static Drawable getBackButtonDrawable() {
|
public static Drawable getBackButtonDrawable() {
|
||||||
final int backButtonResource = Utils.getResourceIdentifierOrThrow(
|
final int backButtonResource = Utils.getResourceIdentifier(
|
||||||
"revanced_settings_toolbar_arrow_left", "drawable");
|
"revanced_settings_toolbar_arrow_left", "drawable");
|
||||||
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
||||||
customizeBackButtonDrawable(drawable);
|
customizeBackButtonDrawable(drawable);
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.search;
|
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.style.BackgroundColorSpan;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract base class for search result items, defining common fields and behavior.
|
|
||||||
*/
|
|
||||||
public abstract class BaseSearchResultItem {
|
|
||||||
// Enum to represent view types.
|
|
||||||
public enum ViewType {
|
|
||||||
REGULAR,
|
|
||||||
SWITCH,
|
|
||||||
LIST,
|
|
||||||
COLOR_PICKER,
|
|
||||||
GROUP_HEADER,
|
|
||||||
NO_RESULTS,
|
|
||||||
URL_LINK;
|
|
||||||
|
|
||||||
// Get the corresponding layout resource ID.
|
|
||||||
public int getLayoutResourceId() {
|
|
||||||
return switch (this) {
|
|
||||||
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
|
|
||||||
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
|
|
||||||
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
|
|
||||||
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
|
|
||||||
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
|
|
||||||
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getResourceIdentifier(String name) {
|
|
||||||
// Placeholder for actual resource identifier retrieval.
|
|
||||||
return Utils.getResourceIdentifierOrThrow(name, "layout");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String navigationPath;
|
|
||||||
final List<String> navigationKeys;
|
|
||||||
final ViewType preferenceType;
|
|
||||||
CharSequence highlightedTitle;
|
|
||||||
CharSequence highlightedSummary;
|
|
||||||
boolean highlightingApplied;
|
|
||||||
|
|
||||||
BaseSearchResultItem(String navPath, List<String> navKeys, ViewType type) {
|
|
||||||
this.navigationPath = navPath;
|
|
||||||
this.navigationKeys = new ArrayList<>(navKeys != null ? navKeys : Collections.emptyList());
|
|
||||||
this.preferenceType = type;
|
|
||||||
this.highlightedTitle = "";
|
|
||||||
this.highlightedSummary = "";
|
|
||||||
this.highlightingApplied = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract boolean matchesQuery(String query);
|
|
||||||
abstract void applyHighlighting(Pattern queryPattern);
|
|
||||||
abstract void clearHighlighting();
|
|
||||||
|
|
||||||
// Shared method for highlighting text with search query.
|
|
||||||
protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
|
||||||
if (TextUtils.isEmpty(text) || queryPattern == null) return text;
|
|
||||||
|
|
||||||
final int adjustedColor = Utils.adjustColorBrightness(
|
|
||||||
Utils.getAppBackgroundColor(), 0.95f, 1.20f);
|
|
||||||
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
|
||||||
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
|
||||||
|
|
||||||
Matcher matcher = queryPattern.matcher(text);
|
|
||||||
while (matcher.find()) {
|
|
||||||
int start = matcher.start();
|
|
||||||
int end = matcher.end();
|
|
||||||
if (start == end) continue; // Skip zero matches.
|
|
||||||
spannable.setSpan(highlightSpan, start, end,
|
|
||||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return spannable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search result item for group headers (navigation path only).
|
|
||||||
*/
|
|
||||||
public static class GroupHeaderItem extends BaseSearchResultItem {
|
|
||||||
GroupHeaderItem(String navPath, List<String> navKeys) {
|
|
||||||
super(navPath, navKeys, ViewType.GROUP_HEADER);
|
|
||||||
this.highlightedTitle = navPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean matchesQuery(String query) {
|
|
||||||
return false; // Headers are not directly searchable.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void applyHighlighting(Pattern queryPattern) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void clearHighlighting() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search result item for preferences, handling type-specific data and search text.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public static class PreferenceSearchItem extends BaseSearchResultItem {
|
|
||||||
public final Preference preference;
|
|
||||||
final String searchableText;
|
|
||||||
final CharSequence originalTitle;
|
|
||||||
final CharSequence originalSummary;
|
|
||||||
final CharSequence originalSummaryOn;
|
|
||||||
final CharSequence originalSummaryOff;
|
|
||||||
final CharSequence[] originalEntries;
|
|
||||||
private CharSequence[] highlightedEntries;
|
|
||||||
private boolean entriesHighlightingApplied;
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
private int color;
|
|
||||||
|
|
||||||
// Store last applied highlighting pattern to reapply when needed.
|
|
||||||
Pattern lastQueryPattern;
|
|
||||||
|
|
||||||
PreferenceSearchItem(Preference pref, String navPath, List<String> navKeys) {
|
|
||||||
super(navPath, navKeys, determineType(pref));
|
|
||||||
this.preference = pref;
|
|
||||||
this.originalTitle = pref.getTitle() != null ? pref.getTitle() : "";
|
|
||||||
this.originalSummary = pref.getSummary();
|
|
||||||
this.highlightedTitle = this.originalTitle;
|
|
||||||
this.highlightedSummary = this.originalSummary != null ? this.originalSummary : "";
|
|
||||||
this.color = 0;
|
|
||||||
this.lastQueryPattern = null;
|
|
||||||
|
|
||||||
// Initialize type-specific fields.
|
|
||||||
FieldInitializationResult result = initTypeSpecificFields(pref);
|
|
||||||
this.originalSummaryOn = result.summaryOn;
|
|
||||||
this.originalSummaryOff = result.summaryOff;
|
|
||||||
this.originalEntries = result.entries;
|
|
||||||
|
|
||||||
// Build searchable text.
|
|
||||||
this.searchableText = buildSearchableText(pref);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class FieldInitializationResult {
|
|
||||||
CharSequence summaryOn = null;
|
|
||||||
CharSequence summaryOff = null;
|
|
||||||
CharSequence[] entries = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ViewType determineType(Preference pref) {
|
|
||||||
if (pref instanceof SwitchPreference) return ViewType.SWITCH;
|
|
||||||
if (pref instanceof ListPreference) return ViewType.LIST;
|
|
||||||
if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
|
|
||||||
if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK;
|
|
||||||
if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
|
|
||||||
return ViewType.REGULAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
private FieldInitializationResult initTypeSpecificFields(Preference pref) {
|
|
||||||
FieldInitializationResult result = new FieldInitializationResult();
|
|
||||||
|
|
||||||
if (pref instanceof SwitchPreference switchPref) {
|
|
||||||
result.summaryOn = switchPref.getSummaryOn();
|
|
||||||
result.summaryOff = switchPref.getSummaryOff();
|
|
||||||
} else if (pref instanceof ColorPickerPreference colorPref) {
|
|
||||||
String colorString = colorPref.getText();
|
|
||||||
this.color = TextUtils.isEmpty(colorString) ? 0 : Color.parseColor(colorString);
|
|
||||||
} else if (pref instanceof ListPreference listPref) {
|
|
||||||
result.entries = listPref.getEntries();
|
|
||||||
if (result.entries != null) {
|
|
||||||
this.highlightedEntries = new CharSequence[result.entries.length];
|
|
||||||
System.arraycopy(result.entries, 0, this.highlightedEntries, 0, result.entries.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.entriesHighlightingApplied = false;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildSearchableText(Preference pref) {
|
|
||||||
StringBuilder searchBuilder = new StringBuilder();
|
|
||||||
String key = pref.getKey();
|
|
||||||
String normalizedKey = "";
|
|
||||||
if (key != null) {
|
|
||||||
// Normalize preference key by removing the common "revanced_" prefix
|
|
||||||
// so that users can search by the meaningful part only.
|
|
||||||
normalizedKey = key.startsWith("revanced_")
|
|
||||||
? key.substring("revanced_".length())
|
|
||||||
: key;
|
|
||||||
}
|
|
||||||
appendText(searchBuilder, normalizedKey);
|
|
||||||
appendText(searchBuilder, originalTitle);
|
|
||||||
appendText(searchBuilder, originalSummary);
|
|
||||||
|
|
||||||
// Add type-specific searchable content.
|
|
||||||
if (pref instanceof ListPreference) {
|
|
||||||
if (originalEntries != null) {
|
|
||||||
for (CharSequence entry : originalEntries) {
|
|
||||||
appendText(searchBuilder, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pref instanceof SwitchPreference) {
|
|
||||||
appendText(searchBuilder, originalSummaryOn);
|
|
||||||
appendText(searchBuilder, originalSummaryOff);
|
|
||||||
} else if (pref instanceof ColorPickerPreference) {
|
|
||||||
appendText(searchBuilder, ColorPickerPreference.getColorString(color, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include navigation path in searchable text.
|
|
||||||
appendText(searchBuilder, navigationPath);
|
|
||||||
|
|
||||||
return searchBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends normalized searchable text to the builder.
|
|
||||||
* Uses full Unicode normalization for accurate search across all languages.
|
|
||||||
*/
|
|
||||||
private void appendText(StringBuilder builder, CharSequence text) {
|
|
||||||
if (!TextUtils.isEmpty(text)) {
|
|
||||||
if (builder.length() > 0) builder.append(" ");
|
|
||||||
builder.append(Utils.normalizeTextToLowercase(text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current effective summary for this preference, considering state-dependent summaries.
|
|
||||||
*/
|
|
||||||
public CharSequence getCurrentEffectiveSummary() {
|
|
||||||
if (preference instanceof CustomDialogListPreference customPref) {
|
|
||||||
String staticSum = customPref.getStaticSummary();
|
|
||||||
if (staticSum != null) {
|
|
||||||
return staticSum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (preference instanceof SwitchPreference switchPref) {
|
|
||||||
boolean currentState = switchPref.isChecked();
|
|
||||||
return currentState
|
|
||||||
? (originalSummaryOn != null ? originalSummaryOn :
|
|
||||||
originalSummary != null ? originalSummary : "")
|
|
||||||
: (originalSummaryOff != null ? originalSummaryOff :
|
|
||||||
originalSummary != null ? originalSummary : "");
|
|
||||||
} else if (preference instanceof ListPreference listPref) {
|
|
||||||
String value = listPref.getValue();
|
|
||||||
CharSequence[] entries = listPref.getEntries();
|
|
||||||
CharSequence[] entryValues = listPref.getEntryValues();
|
|
||||||
if (value != null && entries != null && entryValues != null) {
|
|
||||||
for (int i = 0, length = entries.length; i < length; i++) {
|
|
||||||
if (value.equals(entryValues[i].toString())) {
|
|
||||||
return originalEntries != null && i < originalEntries.length && originalEntries[i] != null
|
|
||||||
? originalEntries[i]
|
|
||||||
: originalSummary != null ? originalSummary : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return originalSummary != null ? originalSummary : "";
|
|
||||||
}
|
|
||||||
return originalSummary != null ? originalSummary : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this search result item matches the provided query.
|
|
||||||
* Uses case-insensitive matching against the searchable text.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
boolean matchesQuery(String query) {
|
|
||||||
return searchableText.contains(Utils.normalizeTextToLowercase(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get highlighted entries to show in dialog.
|
|
||||||
*/
|
|
||||||
public CharSequence[] getHighlightedEntries() {
|
|
||||||
return highlightedEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether highlighting is applied to entries.
|
|
||||||
*/
|
|
||||||
public boolean isEntriesHighlightingApplied() {
|
|
||||||
return entriesHighlightingApplied;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights the search query in the title and summary.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
void applyHighlighting(Pattern queryPattern) {
|
|
||||||
this.lastQueryPattern = queryPattern;
|
|
||||||
// Highlight the title.
|
|
||||||
highlightedTitle = highlightSearchQuery(originalTitle, queryPattern);
|
|
||||||
|
|
||||||
// Get the current effective summary and highlight it.
|
|
||||||
CharSequence currentSummary = getCurrentEffectiveSummary();
|
|
||||||
highlightedSummary = highlightSearchQuery(currentSummary, queryPattern);
|
|
||||||
|
|
||||||
// Highlight the entries.
|
|
||||||
if (preference instanceof ListPreference && originalEntries != null) {
|
|
||||||
highlightedEntries = new CharSequence[originalEntries.length];
|
|
||||||
for (int i = 0, length = originalEntries.length; i < length; i++) {
|
|
||||||
if (originalEntries[i] != null) {
|
|
||||||
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
|
|
||||||
} else {
|
|
||||||
highlightedEntries[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entriesHighlightingApplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightingApplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all search query highlighting and restores original state completely.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
void clearHighlighting() {
|
|
||||||
if (!highlightingApplied) return;
|
|
||||||
|
|
||||||
// Restore original title.
|
|
||||||
highlightedTitle = originalTitle;
|
|
||||||
|
|
||||||
// Restore current effective summary without highlighting.
|
|
||||||
highlightedSummary = getCurrentEffectiveSummary();
|
|
||||||
|
|
||||||
// Restore original entries.
|
|
||||||
if (originalEntries != null && highlightedEntries != null) {
|
|
||||||
System.arraycopy(originalEntries, 0, highlightedEntries, 0,
|
|
||||||
Math.min(originalEntries.length, highlightedEntries.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
entriesHighlightingApplied = false;
|
|
||||||
highlightingApplied = false;
|
|
||||||
lastQueryPattern = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes highlighting for dynamic summaries (like switch preferences).
|
|
||||||
* Should be called when the preference state changes.
|
|
||||||
*/
|
|
||||||
public void refreshHighlighting() {
|
|
||||||
if (highlightingApplied && lastQueryPattern != null) {
|
|
||||||
CharSequence currentSummary = getCurrentEffectiveSummary();
|
|
||||||
highlightedSummary = highlightSearchQuery(currentSummary, lastQueryPattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(int newColor) {
|
|
||||||
this.color = newColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
public int getColor() {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,621 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.search;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
import static app.revanced.extension.shared.settings.search.BaseSearchViewController.DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
|
|
||||||
|
|
||||||
import android.animation.AnimatorSet;
|
|
||||||
import android.animation.ArgbEvaluator;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceGroup;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ListAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Switch;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
|
||||||
import app.revanced.extension.shared.ui.ColorDot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract adapter for displaying search results in overlay ListView with ViewHolder pattern.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchResultItem> {
|
|
||||||
protected final LayoutInflater inflater;
|
|
||||||
protected final BaseSearchViewController.BasePreferenceFragment fragment;
|
|
||||||
protected final BaseSearchViewController searchViewController;
|
|
||||||
protected AnimatorSet currentAnimator;
|
|
||||||
protected abstract PreferenceScreen getMainPreferenceScreen();
|
|
||||||
|
|
||||||
protected static final int BLINK_DURATION = 400;
|
|
||||||
protected static final int PAUSE_BETWEEN_BLINKS = 100;
|
|
||||||
|
|
||||||
protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
|
|
||||||
"preference_title", "id");
|
|
||||||
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
|
|
||||||
"preference_summary", "id");
|
|
||||||
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
|
|
||||||
"preference_path", "id");
|
|
||||||
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
|
|
||||||
"preference_switch", "id");
|
|
||||||
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
|
|
||||||
"preference_color_dot", "id");
|
|
||||||
|
|
||||||
protected static class RegularViewHolder {
|
|
||||||
TextView titleView;
|
|
||||||
TextView summaryView;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class SwitchViewHolder {
|
|
||||||
TextView titleView;
|
|
||||||
TextView summaryView;
|
|
||||||
Switch switchWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class ColorViewHolder {
|
|
||||||
TextView titleView;
|
|
||||||
TextView summaryView;
|
|
||||||
View colorDot;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class GroupHeaderViewHolder {
|
|
||||||
TextView pathView;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class NoResultsViewHolder {
|
|
||||||
TextView titleView;
|
|
||||||
TextView summaryView;
|
|
||||||
ImageView iconView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BaseSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
|
||||||
BaseSearchViewController.BasePreferenceFragment fragment,
|
|
||||||
BaseSearchViewController searchViewController) {
|
|
||||||
super(context, 0, items);
|
|
||||||
this.inflater = LayoutInflater.from(context);
|
|
||||||
this.fragment = fragment;
|
|
||||||
this.searchViewController = searchViewController;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
BaseSearchResultItem item = getItem(position);
|
|
||||||
return item == null ? 0 : item.preferenceType.ordinal();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getViewTypeCount() {
|
|
||||||
return BaseSearchResultItem.ViewType.values().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
|
||||||
BaseSearchResultItem item = getItem(position);
|
|
||||||
if (item == null) return new View(getContext());
|
|
||||||
// Use the ViewType enum.
|
|
||||||
BaseSearchResultItem.ViewType viewType = item.preferenceType;
|
|
||||||
// Create or reuse preference view based on type.
|
|
||||||
return createPreferenceView(item, convertView, viewType, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled(int position) {
|
|
||||||
BaseSearchResultItem item = getItem(position);
|
|
||||||
// Disable for NO_RESULTS items to prevent ripple/selection.
|
|
||||||
return item != null && item.preferenceType != BaseSearchResultItem.ViewType.NO_RESULTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates or reuses a view for the given SearchResultItem.
|
|
||||||
* <p>
|
|
||||||
* Thanks to {@link #getItemViewType(int)} and {@link #getViewTypeCount()}, ListView knows
|
|
||||||
* how many different row types exist and keeps a separate "recycling pool" for each.
|
|
||||||
* That means convertView passed here is ALWAYS of the correct type for this position.
|
|
||||||
* So only need to check if (view == null), and if so – inflate a new layout and create the proper ViewHolder.
|
|
||||||
*/
|
|
||||||
protected View createPreferenceView(BaseSearchResultItem item, View convertView,
|
|
||||||
BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
|
|
||||||
View view = convertView;
|
|
||||||
if (view == null) {
|
|
||||||
view = inflateViewForType(viewType, parent);
|
|
||||||
createViewHolderForType(view, viewType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the cached ViewHolder.
|
|
||||||
Object holder = view.getTag();
|
|
||||||
bindDataToViewHolder(item, holder, viewType, view);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected View inflateViewForType(BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
|
|
||||||
return inflater.inflate(viewType.getLayoutResourceId(), parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void createViewHolderForType(View view, BaseSearchResultItem.ViewType viewType) {
|
|
||||||
switch (viewType) {
|
|
||||||
case REGULAR, LIST, URL_LINK -> {
|
|
||||||
RegularViewHolder regularHolder = new RegularViewHolder();
|
|
||||||
regularHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
|
||||||
regularHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
|
||||||
view.setTag(regularHolder);
|
|
||||||
}
|
|
||||||
case SWITCH -> {
|
|
||||||
SwitchViewHolder switchHolder = new SwitchViewHolder();
|
|
||||||
switchHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
|
||||||
switchHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
|
||||||
switchHolder.switchWidget = view.findViewById(ID_PREFERENCE_SWITCH);
|
|
||||||
view.setTag(switchHolder);
|
|
||||||
}
|
|
||||||
case COLOR_PICKER -> {
|
|
||||||
ColorViewHolder colorHolder = new ColorViewHolder();
|
|
||||||
colorHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
|
||||||
colorHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
|
||||||
colorHolder.colorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
|
||||||
view.setTag(colorHolder);
|
|
||||||
}
|
|
||||||
case GROUP_HEADER -> {
|
|
||||||
GroupHeaderViewHolder groupHolder = new GroupHeaderViewHolder();
|
|
||||||
groupHolder.pathView = view.findViewById(ID_PREFERENCE_PATH);
|
|
||||||
view.setTag(groupHolder);
|
|
||||||
}
|
|
||||||
case NO_RESULTS -> {
|
|
||||||
NoResultsViewHolder noResultsHolder = new NoResultsViewHolder();
|
|
||||||
noResultsHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
|
||||||
noResultsHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
|
||||||
noResultsHolder.iconView = view.findViewById(android.R.id.icon);
|
|
||||||
view.setTag(noResultsHolder);
|
|
||||||
}
|
|
||||||
default -> throw new IllegalStateException("Unknown viewType: " + viewType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindDataToViewHolder(BaseSearchResultItem item, Object holder,
|
|
||||||
BaseSearchResultItem.ViewType viewType, View view) {
|
|
||||||
switch (viewType) {
|
|
||||||
case REGULAR, URL_LINK, LIST -> bindRegularViewHolder(item, (RegularViewHolder) holder, view);
|
|
||||||
case SWITCH -> bindSwitchViewHolder(item, (SwitchViewHolder) holder, view);
|
|
||||||
case COLOR_PICKER -> bindColorViewHolder(item, (ColorViewHolder) holder, view);
|
|
||||||
case GROUP_HEADER -> bindGroupHeaderViewHolder(item, (GroupHeaderViewHolder) holder, view);
|
|
||||||
case NO_RESULTS -> bindNoResultsViewHolder(item, (NoResultsViewHolder) holder);
|
|
||||||
default -> throw new IllegalStateException("Unknown viewType: " + viewType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindRegularViewHolder(BaseSearchResultItem item, RegularViewHolder holder, View view) {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
|
||||||
prefItem.refreshHighlighting();
|
|
||||||
holder.titleView.setText(item.highlightedTitle);
|
|
||||||
holder.summaryView.setText(item.highlightedSummary);
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
|
|
||||||
() -> {
|
|
||||||
handlePreferenceClick(prefItem.preference);
|
|
||||||
if (prefItem.preference instanceof ListPreference) {
|
|
||||||
prefItem.refreshHighlighting();
|
|
||||||
holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() -> navigateAndScrollToPreference(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindSwitchViewHolder(BaseSearchResultItem item, SwitchViewHolder holder, View view) {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
|
||||||
SwitchPreference switchPref = (SwitchPreference) prefItem.preference;
|
|
||||||
holder.titleView.setText(item.highlightedTitle);
|
|
||||||
holder.switchWidget.setBackground(null); // Remove ripple/highlight.
|
|
||||||
// Sync switch state with preference without animation.
|
|
||||||
boolean currentState = switchPref.isChecked();
|
|
||||||
if (holder.switchWidget.isChecked() != currentState) {
|
|
||||||
holder.switchWidget.setChecked(currentState);
|
|
||||||
holder.switchWidget.jumpDrawablesToCurrentState();
|
|
||||||
}
|
|
||||||
prefItem.refreshHighlighting();
|
|
||||||
holder.summaryView.setText(prefItem.highlightedSummary);
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
setupPreferenceView(view, holder.titleView, holder.summaryView, switchPref,
|
|
||||||
() -> {
|
|
||||||
boolean newState = !switchPref.isChecked();
|
|
||||||
switchPref.setChecked(newState);
|
|
||||||
holder.switchWidget.setChecked(newState);
|
|
||||||
prefItem.refreshHighlighting();
|
|
||||||
holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
if (switchPref.getOnPreferenceChangeListener() != null) {
|
|
||||||
switchPref.getOnPreferenceChangeListener().onPreferenceChange(switchPref, newState);
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
},
|
|
||||||
() -> navigateAndScrollToPreference(item));
|
|
||||||
holder.switchWidget.setEnabled(switchPref.isEnabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindColorViewHolder(BaseSearchResultItem item, ColorViewHolder holder, View view) {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
|
||||||
holder.titleView.setText(item.highlightedTitle);
|
|
||||||
holder.summaryView.setText(item.highlightedSummary);
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
ColorDot.applyColorDot(holder.colorDot, prefItem.getColor(), prefItem.preference.isEnabled());
|
|
||||||
setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
|
|
||||||
() -> handlePreferenceClick(prefItem.preference),
|
|
||||||
() -> navigateAndScrollToPreference(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindGroupHeaderViewHolder(BaseSearchResultItem item, GroupHeaderViewHolder holder, View view) {
|
|
||||||
holder.pathView.setText(item.highlightedTitle);
|
|
||||||
view.setOnClickListener(v -> navigateToTargetScreen(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void bindNoResultsViewHolder(BaseSearchResultItem item, NoResultsViewHolder holder) {
|
|
||||||
holder.titleView.setText(item.highlightedTitle);
|
|
||||||
holder.summaryView.setText(item.highlightedSummary);
|
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
|
||||||
holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up a preference view with click listeners and proper enabled state handling.
|
|
||||||
*/
|
|
||||||
protected void setupPreferenceView(View view, TextView titleView, TextView summaryView, Preference preference,
|
|
||||||
Runnable onClickAction, Runnable onLongClickAction) {
|
|
||||||
boolean enabled = preference.isEnabled();
|
|
||||||
|
|
||||||
// To enable long-click navigation for disabled settings, manually control the enabled state of the title
|
|
||||||
// and summary and disable the ripple effect instead of using 'view.setEnabled(enabled)'.
|
|
||||||
|
|
||||||
titleView.setEnabled(enabled);
|
|
||||||
summaryView.setEnabled(enabled);
|
|
||||||
|
|
||||||
if (!enabled) view.setBackground(null); // Disable ripple effect.
|
|
||||||
|
|
||||||
// In light mode, alpha 0.5 is applied to a disabled title automatically,
|
|
||||||
// but in dark mode it needs to be applied manually.
|
|
||||||
if (Utils.isDarkModeEnabled()) {
|
|
||||||
titleView.setAlpha(enabled ? 1.0f : ColorPickerPreference.DISABLED_ALPHA);
|
|
||||||
}
|
|
||||||
// Set up click and long-click listeners.
|
|
||||||
view.setOnClickListener(enabled ? v -> onClickAction.run() : null);
|
|
||||||
view.setOnLongClickListener(v -> {
|
|
||||||
onLongClickAction.run();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the settings screen containing the given search result item and triggers scrolling.
|
|
||||||
*/
|
|
||||||
protected void navigateAndScrollToPreference(BaseSearchResultItem item) {
|
|
||||||
// No navigation for URL_LINK items.
|
|
||||||
if (item.preferenceType == BaseSearchResultItem.ViewType.URL_LINK) return;
|
|
||||||
|
|
||||||
PreferenceScreen targetScreen = navigateToTargetScreen(item);
|
|
||||||
if (targetScreen == null) return;
|
|
||||||
if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) return;
|
|
||||||
|
|
||||||
Preference targetPreference = prefItem.preference;
|
|
||||||
|
|
||||||
fragment.getView().post(() -> {
|
|
||||||
ListView listView = targetScreen == getMainPreferenceScreen()
|
|
||||||
? getPreferenceListView()
|
|
||||||
: targetScreen.getDialog().findViewById(android.R.id.list);
|
|
||||||
|
|
||||||
if (listView == null) return;
|
|
||||||
|
|
||||||
int targetPosition = findPreferencePosition(targetPreference, listView);
|
|
||||||
if (targetPosition == -1) return;
|
|
||||||
|
|
||||||
int firstVisible = listView.getFirstVisiblePosition();
|
|
||||||
int lastVisible = listView.getLastVisiblePosition();
|
|
||||||
|
|
||||||
if (targetPosition >= firstVisible && targetPosition <= lastVisible) {
|
|
||||||
// The preference is already visible, but still scroll it to the bottom of the list for consistency.
|
|
||||||
View child = listView.getChildAt(targetPosition - firstVisible);
|
|
||||||
if (child != null) {
|
|
||||||
// Calculate how much to scroll so the item is aligned at the bottom.
|
|
||||||
int scrollAmount = child.getBottom() - listView.getHeight();
|
|
||||||
if (scrollAmount > 0) {
|
|
||||||
// Perform smooth scroll animation for better user experience.
|
|
||||||
listView.smoothScrollBy(scrollAmount, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Highlight the preference once it is positioned.
|
|
||||||
highlightPreferenceAtPosition(listView, targetPosition);
|
|
||||||
} else {
|
|
||||||
// The preference is outside of the current visible range, scroll to it from the top.
|
|
||||||
listView.smoothScrollToPositionFromTop(targetPosition, 0);
|
|
||||||
|
|
||||||
Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
// Fallback runnable in case the OnScrollListener does not trigger.
|
|
||||||
Runnable fallback = () -> {
|
|
||||||
listView.setOnScrollListener(null);
|
|
||||||
highlightPreferenceAtPosition(listView, targetPosition);
|
|
||||||
};
|
|
||||||
// Post fallback with a small delay.
|
|
||||||
handler.postDelayed(fallback, 350);
|
|
||||||
|
|
||||||
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
|
|
||||||
private boolean isScrolling = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
|
||||||
if (scrollState == SCROLL_STATE_TOUCH_SCROLL || scrollState == SCROLL_STATE_FLING) {
|
|
||||||
// Mark that scrolling has started.
|
|
||||||
isScrolling = true;
|
|
||||||
}
|
|
||||||
if (scrollState == SCROLL_STATE_IDLE && isScrolling) {
|
|
||||||
// Scrolling is finished, cleanup listener and cancel fallback.
|
|
||||||
isScrolling = false;
|
|
||||||
listView.setOnScrollListener(null);
|
|
||||||
handler.removeCallbacks(fallback);
|
|
||||||
// Highlight the target preference when scrolling is done.
|
|
||||||
highlightPreferenceAtPosition(listView, targetPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the final PreferenceScreen using preference keys or titles as fallback.
|
|
||||||
*/
|
|
||||||
protected PreferenceScreen navigateToTargetScreen(BaseSearchResultItem item) {
|
|
||||||
PreferenceScreen currentScreen = getMainPreferenceScreen();
|
|
||||||
Preference targetPref = null;
|
|
||||||
|
|
||||||
// Try key-based navigation first.
|
|
||||||
if (item.navigationKeys != null && !item.navigationKeys.isEmpty()) {
|
|
||||||
String finalKey = item.navigationKeys.get(item.navigationKeys.size() - 1);
|
|
||||||
targetPref = findPreferenceByKey(currentScreen, finalKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to title-based navigation.
|
|
||||||
if (targetPref == null && !TextUtils.isEmpty(item.navigationPath)) {
|
|
||||||
String[] pathSegments = item.navigationPath.split(" > ");
|
|
||||||
String finalSegment = pathSegments[pathSegments.length - 1].trim();
|
|
||||||
if (!TextUtils.isEmpty(finalSegment)) {
|
|
||||||
targetPref = findPreferenceByTitle(currentScreen, finalSegment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetPref instanceof PreferenceScreen targetScreen) {
|
|
||||||
handlePreferenceClick(targetScreen);
|
|
||||||
return targetScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively searches for a preference by title in a preference group.
|
|
||||||
*/
|
|
||||||
protected Preference findPreferenceByTitle(PreferenceGroup group, String title) {
|
|
||||||
for (int i = 0; i < group.getPreferenceCount(); i++) {
|
|
||||||
Preference pref = group.getPreference(i);
|
|
||||||
CharSequence prefTitle = pref.getTitle();
|
|
||||||
if (prefTitle != null && (prefTitle.toString().trim().equalsIgnoreCase(title)
|
|
||||||
|| normalizeString(prefTitle.toString()).equals(normalizeString(title)))) {
|
|
||||||
return pref;
|
|
||||||
}
|
|
||||||
if (pref instanceof PreferenceGroup) {
|
|
||||||
Preference found = findPreferenceByTitle((PreferenceGroup) pref, title);
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes string for comparison (removes extra characters, spaces etc).
|
|
||||||
*/
|
|
||||||
protected String normalizeString(String input) {
|
|
||||||
if (TextUtils.isEmpty(input)) return "";
|
|
||||||
return input.trim().toLowerCase().replaceAll("\\s+", " ").replaceAll("[^\\w\\s]", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the ListView from the PreferenceFragment.
|
|
||||||
*/
|
|
||||||
protected ListView getPreferenceListView() {
|
|
||||||
View fragmentView = fragment.getView();
|
|
||||||
if (fragmentView != null) {
|
|
||||||
ListView listView = findListViewInViewGroup(fragmentView);
|
|
||||||
if (listView != null) {
|
|
||||||
return listView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fragment.getActivity().findViewById(android.R.id.list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively searches for a ListView in a ViewGroup.
|
|
||||||
*/
|
|
||||||
protected ListView findListViewInViewGroup(View view) {
|
|
||||||
if (view instanceof ListView) {
|
|
||||||
return (ListView) view;
|
|
||||||
}
|
|
||||||
if (view instanceof ViewGroup group) {
|
|
||||||
for (int i = 0; i < group.getChildCount(); i++) {
|
|
||||||
ListView result = findListViewInViewGroup(group.getChildAt(i));
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the position of a preference in the ListView adapter.
|
|
||||||
*/
|
|
||||||
protected int findPreferencePosition(Preference targetPreference, ListView listView) {
|
|
||||||
ListAdapter adapter = listView.getAdapter();
|
|
||||||
if (adapter == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0, count = adapter.getCount(); i < count; i++) {
|
|
||||||
Object item = adapter.getItem(i);
|
|
||||||
if (item == targetPreference) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
if (item instanceof Preference pref && targetPreference.getKey() != null) {
|
|
||||||
if (targetPreference.getKey().equals(pref.getKey())) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights a preference at the specified position with a blink effect.
|
|
||||||
*/
|
|
||||||
protected void highlightPreferenceAtPosition(ListView listView, int position) {
|
|
||||||
int firstVisible = listView.getFirstVisiblePosition();
|
|
||||||
if (position < firstVisible || position > listView.getLastVisiblePosition()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
View itemView = listView.getChildAt(position - firstVisible);
|
|
||||||
if (itemView != null) {
|
|
||||||
blinkView(itemView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a smooth double-blink effect on a view's background without affecting the text.
|
|
||||||
* @param view The View to apply the animation to.
|
|
||||||
*/
|
|
||||||
protected void blinkView(View view) {
|
|
||||||
// If a previous animation is still running, cancel it to prevent conflicts.
|
|
||||||
if (currentAnimator != null && currentAnimator.isRunning()) {
|
|
||||||
currentAnimator.cancel();
|
|
||||||
}
|
|
||||||
final int startColor = Utils.getAppBackgroundColor();
|
|
||||||
final int highlightColor = Utils.adjustColorBrightness(
|
|
||||||
startColor,
|
|
||||||
Utils.isDarkModeEnabled() ? 1.25f : 0.8f
|
|
||||||
);
|
|
||||||
// Animator for transitioning from the start color to the highlight color.
|
|
||||||
ObjectAnimator fadeIn = ObjectAnimator.ofObject(
|
|
||||||
view,
|
|
||||||
"backgroundColor",
|
|
||||||
new ArgbEvaluator(),
|
|
||||||
startColor,
|
|
||||||
highlightColor
|
|
||||||
);
|
|
||||||
fadeIn.setDuration(BLINK_DURATION);
|
|
||||||
// Animator to return to the start color.
|
|
||||||
ObjectAnimator fadeOut = ObjectAnimator.ofObject(
|
|
||||||
view,
|
|
||||||
"backgroundColor",
|
|
||||||
new ArgbEvaluator(),
|
|
||||||
highlightColor,
|
|
||||||
startColor
|
|
||||||
);
|
|
||||||
fadeOut.setDuration(BLINK_DURATION);
|
|
||||||
|
|
||||||
currentAnimator = new AnimatorSet();
|
|
||||||
// Create the sequence: fadeIn -> fadeOut -> (pause) -> fadeIn -> fadeOut.
|
|
||||||
AnimatorSet firstBlink = new AnimatorSet();
|
|
||||||
firstBlink.playSequentially(fadeIn, fadeOut);
|
|
||||||
AnimatorSet secondBlink = new AnimatorSet();
|
|
||||||
secondBlink.playSequentially(fadeIn.clone(), fadeOut.clone()); // Use clones for the second blink.
|
|
||||||
|
|
||||||
currentAnimator.play(secondBlink).after(firstBlink).after(PAUSE_BETWEEN_BLINKS);
|
|
||||||
currentAnimator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively finds a preference by key in a preference group.
|
|
||||||
*/
|
|
||||||
protected Preference findPreferenceByKey(PreferenceGroup group, String key) {
|
|
||||||
if (group == null || TextUtils.isEmpty(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First search on current level.
|
|
||||||
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
|
||||||
Preference pref = group.getPreference(i);
|
|
||||||
if (key.equals(pref.getKey())) {
|
|
||||||
return pref;
|
|
||||||
}
|
|
||||||
if (pref instanceof PreferenceGroup) {
|
|
||||||
Preference found = findPreferenceByKey((PreferenceGroup) pref, key);
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles preference click actions by invoking the preference's performClick method via reflection.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("all")
|
|
||||||
private void handlePreferenceClick(Preference preference) {
|
|
||||||
try {
|
|
||||||
if (preference instanceof CustomDialogListPreference listPref) {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
|
||||||
searchViewController.findSearchItemByPreference(preference);
|
|
||||||
if (searchItem != null && searchItem.isEntriesHighlightingApplied()) {
|
|
||||||
listPref.setHighlightedEntriesForDialog(searchItem.getHighlightedEntries());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Method m = Preference.class.getDeclaredMethod("performClick", PreferenceScreen.class);
|
|
||||||
m.setAccessible(true);
|
|
||||||
m.invoke(preference, fragment.getPreferenceScreenForSearch());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.printException(() -> "Failed to invoke performClick()", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a preference has navigation capability (can open a new screen).
|
|
||||||
*/
|
|
||||||
boolean hasNavigationCapability(Preference preference) {
|
|
||||||
// PreferenceScreen always allows navigation.
|
|
||||||
if (preference instanceof PreferenceScreen) return true;
|
|
||||||
// UrlLinkPreference does not navigate to a new screen, it opens an external URL.
|
|
||||||
if (preference instanceof UrlLinkPreference) return false;
|
|
||||||
// Other group types that might have their own screens.
|
|
||||||
if (preference instanceof PreferenceGroup) {
|
|
||||||
// Check if it has its own fragment or intent.
|
|
||||||
return preference.getIntent() != null || preference.getFragment() != null;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.search;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceGroup;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract controller for managing the overlay search view in ReVanced settings.
|
|
||||||
* Subclasses must implement app-specific preference handling.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public abstract class BaseSearchViewController {
|
|
||||||
protected SearchView searchView;
|
|
||||||
protected FrameLayout searchContainer;
|
|
||||||
protected FrameLayout overlayContainer;
|
|
||||||
protected final Toolbar toolbar;
|
|
||||||
protected final Activity activity;
|
|
||||||
protected final BasePreferenceFragment fragment;
|
|
||||||
protected final CharSequence originalTitle;
|
|
||||||
protected BaseSearchResultsAdapter searchResultsAdapter;
|
|
||||||
protected final List<BaseSearchResultItem> allSearchItems;
|
|
||||||
protected final List<BaseSearchResultItem> filteredSearchItems;
|
|
||||||
protected final Map<String, BaseSearchResultItem> keyToSearchItem;
|
|
||||||
protected final InputMethodManager inputMethodManager;
|
|
||||||
protected SearchHistoryManager searchHistoryManager;
|
|
||||||
protected boolean isSearchActive;
|
|
||||||
protected boolean isShowingSearchHistory;
|
|
||||||
|
|
||||||
protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
|
|
||||||
|
|
||||||
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow("revanced_search_view", "id");
|
|
||||||
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow("revanced_search_view_container", "id");
|
|
||||||
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow("action_search", "id");
|
|
||||||
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
|
||||||
public static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON =
|
|
||||||
getResourceIdentifierOrThrow("revanced_settings_search_icon", "drawable");
|
|
||||||
protected static final int MENU_REVANCED_SEARCH_MENU =
|
|
||||||
getResourceIdentifierOrThrow("revanced_search_menu", "menu");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new BaseSearchViewController instance.
|
|
||||||
*
|
|
||||||
* @param activity The activity hosting the search view.
|
|
||||||
* @param toolbar The toolbar containing the search action.
|
|
||||||
* @param fragment The preference fragment to manage search preferences.
|
|
||||||
*/
|
|
||||||
protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePreferenceFragment fragment) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.toolbar = toolbar;
|
|
||||||
this.fragment = fragment;
|
|
||||||
this.originalTitle = toolbar.getTitle();
|
|
||||||
this.allSearchItems = new ArrayList<>();
|
|
||||||
this.filteredSearchItems = new ArrayList<>();
|
|
||||||
this.keyToSearchItem = new HashMap<>();
|
|
||||||
this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
this.isShowingSearchHistory = false;
|
|
||||||
|
|
||||||
// Initialize components
|
|
||||||
initializeSearchView();
|
|
||||||
initializeOverlayContainer();
|
|
||||||
initializeSearchHistoryManager();
|
|
||||||
setupToolbarMenu();
|
|
||||||
setupListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the search view with proper configurations, such as background, query hint, and RTL support.
|
|
||||||
*/
|
|
||||||
private void initializeSearchView() {
|
|
||||||
// Retrieve SearchView and container from XML.
|
|
||||||
searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
|
|
||||||
EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
|
|
||||||
"android:id/search_src_text", null));
|
|
||||||
// Disable fullscreen keyboard mode.
|
|
||||||
searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
|
||||||
|
|
||||||
searchContainer = activity.findViewById(ID_REVANCED_SEARCH_VIEW_CONTAINER);
|
|
||||||
|
|
||||||
// Set background and query hint.
|
|
||||||
searchView.setBackground(createBackgroundDrawable());
|
|
||||||
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
|
||||||
|
|
||||||
// Set text size.
|
|
||||||
searchEditText.setTextSize(16);
|
|
||||||
|
|
||||||
// Set cursor color.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
setCursorColor(searchEditText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure RTL support based on app language.
|
|
||||||
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
|
|
||||||
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
|
|
||||||
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the cursor color (for Android 10+ devices).
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
|
||||||
private void setCursorColor(EditText editText) {
|
|
||||||
// Get the cursor color based on the current theme.
|
|
||||||
final int cursorColor = Utils.isDarkModeEnabled() ? Color.WHITE : Color.BLACK;
|
|
||||||
|
|
||||||
// Create cursor drawable.
|
|
||||||
GradientDrawable cursorDrawable = new GradientDrawable();
|
|
||||||
cursorDrawable.setShape(GradientDrawable.RECTANGLE);
|
|
||||||
cursorDrawable.setSize(Dim.dp2, -1); // Width: 2dp, Height: match text height.
|
|
||||||
cursorDrawable.setColor(cursorColor);
|
|
||||||
|
|
||||||
// Set cursor drawable.
|
|
||||||
editText.setTextCursorDrawable(cursorDrawable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the overlay container for displaying search results and history.
|
|
||||||
*/
|
|
||||||
private void initializeOverlayContainer() {
|
|
||||||
// Create overlay container for search results and history.
|
|
||||||
overlayContainer = new FrameLayout(activity);
|
|
||||||
overlayContainer.setVisibility(View.GONE);
|
|
||||||
overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
|
|
||||||
overlayContainer.setElevation(Dim.dp8);
|
|
||||||
|
|
||||||
// Container for search results.
|
|
||||||
FrameLayout searchResultsContainer = new FrameLayout(activity);
|
|
||||||
searchResultsContainer.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// Create a ListView for the results.
|
|
||||||
ListView searchResultsListView = new ListView(activity);
|
|
||||||
searchResultsListView.setDivider(null);
|
|
||||||
searchResultsListView.setDividerHeight(0);
|
|
||||||
searchResultsAdapter = createSearchResultsAdapter();
|
|
||||||
searchResultsListView.setAdapter(searchResultsAdapter);
|
|
||||||
|
|
||||||
// Add results list into container.
|
|
||||||
searchResultsContainer.addView(searchResultsListView, new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT));
|
|
||||||
|
|
||||||
// Add results container into overlay.
|
|
||||||
overlayContainer.addView(searchResultsContainer, new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT));
|
|
||||||
|
|
||||||
// Add overlay to the main content container.
|
|
||||||
FrameLayout mainContainer = activity.findViewById(ID_REVANCED_SETTINGS_FRAGMENTS);
|
|
||||||
if (mainContainer != null) {
|
|
||||||
FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT);
|
|
||||||
overlayParams.gravity = Gravity.TOP;
|
|
||||||
mainContainer.addView(overlayContainer, overlayParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the search history manager with the specified overlay container and listener.
|
|
||||||
*/
|
|
||||||
private void initializeSearchHistoryManager() {
|
|
||||||
searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> {
|
|
||||||
searchView.setQuery(query, true);
|
|
||||||
hideSearchHistory();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abstract methods that subclasses must implement.
|
|
||||||
protected abstract BaseSearchResultsAdapter createSearchResultsAdapter();
|
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
||||||
protected abstract boolean isSpecialPreferenceGroup(Preference preference);
|
|
||||||
protected abstract void setupSpecialPreferenceListeners(BaseSearchResultItem item);
|
|
||||||
|
|
||||||
// Abstract interface for preference fragments.
|
|
||||||
public interface BasePreferenceFragment {
|
|
||||||
PreferenceScreen getPreferenceScreenForSearch();
|
|
||||||
android.view.View getView();
|
|
||||||
Activity getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether a preference should be included in the search index.
|
|
||||||
*
|
|
||||||
* @param preference The preference to evaluate.
|
|
||||||
* @param currentDepth The current depth in the preference hierarchy.
|
|
||||||
* @param includeDepth The maximum depth to include in the search index.
|
|
||||||
* @return True if the preference should be included, false otherwise.
|
|
||||||
*/
|
|
||||||
protected boolean shouldIncludePreference(Preference preference, int currentDepth, int includeDepth) {
|
|
||||||
return includeDepth <= currentDepth
|
|
||||||
&& !(preference instanceof PreferenceCategory)
|
|
||||||
&& !isSpecialPreferenceGroup(preference)
|
|
||||||
&& !(preference instanceof PreferenceScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the toolbar menu for the search action.
|
|
||||||
*/
|
|
||||||
protected void setupToolbarMenu() {
|
|
||||||
toolbar.inflateMenu(MENU_REVANCED_SEARCH_MENU);
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
|
||||||
if (item.getItemId() == ID_ACTION_SEARCH && !isSearchActive) {
|
|
||||||
openSearch();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures listeners for the search view and toolbar navigation.
|
|
||||||
*/
|
|
||||||
protected void setupListeners() {
|
|
||||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String query) {
|
|
||||||
try {
|
|
||||||
String queryTrimmed = query.trim();
|
|
||||||
if (!queryTrimmed.isEmpty()) {
|
|
||||||
searchHistoryManager.saveSearchQuery(queryTrimmed);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onQueryTextSubmit failure", ex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String newText) {
|
|
||||||
try {
|
|
||||||
Logger.printDebug(() -> "Search query: " + newText);
|
|
||||||
|
|
||||||
String trimmedText = newText.trim();
|
|
||||||
if (!isSearchActive) {
|
|
||||||
Logger.printDebug(() -> "Search is not active, skipping query processing");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmedText.isEmpty()) {
|
|
||||||
// If empty query: show history.
|
|
||||||
hideSearchResults();
|
|
||||||
showSearchHistory();
|
|
||||||
} else {
|
|
||||||
// If has search text: hide history and show search results.
|
|
||||||
hideSearchHistory();
|
|
||||||
filterAndShowResults(newText);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onQueryTextChange failure", ex);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Set navigation click listener.
|
|
||||||
toolbar.setNavigationOnClickListener(view -> {
|
|
||||||
if (isSearchActive) {
|
|
||||||
closeSearch();
|
|
||||||
} else {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes search data by collecting all searchable preferences from the fragment.
|
|
||||||
* This method should be called after the preference fragment is fully loaded.
|
|
||||||
* Runs on the UI thread to ensure proper access to preference components.
|
|
||||||
*/
|
|
||||||
public void initializeSearchData() {
|
|
||||||
allSearchItems.clear();
|
|
||||||
keyToSearchItem.clear();
|
|
||||||
// Wait until fragment is properly initialized.
|
|
||||||
activity.runOnUiThread(() -> {
|
|
||||||
try {
|
|
||||||
PreferenceScreen screen = fragment.getPreferenceScreenForSearch();
|
|
||||||
if (screen != null) {
|
|
||||||
collectSearchablePreferences(screen);
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
|
||||||
String key = prefItem.preference.getKey();
|
|
||||||
if (key != null) {
|
|
||||||
keyToSearchItem.put(key, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupPreferenceListeners();
|
|
||||||
Logger.printDebug(() -> "Collected " + allSearchItems.size() + " searchable preferences");
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Failed to initialize search data", ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up listeners for preferences to keep search results in sync when preference values change.
|
|
||||||
*/
|
|
||||||
protected void setupPreferenceListeners() {
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
// Skip non-preference items.
|
|
||||||
if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) continue;
|
|
||||||
Preference pref = prefItem.preference;
|
|
||||||
|
|
||||||
if (pref instanceof ColorPickerPreference colorPref) {
|
|
||||||
colorPref.setOnColorChangeListener((prefKey, newColor) -> {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
|
||||||
(BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(prefKey);
|
|
||||||
if (searchItem != null) {
|
|
||||||
searchItem.setColor(newColor);
|
|
||||||
refreshSearchResults();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (pref instanceof CustomDialogListPreference listPref) {
|
|
||||||
listPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
|
||||||
(BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(preference.getKey());
|
|
||||||
if (searchItem == null) return true;
|
|
||||||
|
|
||||||
int index = listPref.findIndexOfValue(newValue.toString());
|
|
||||||
if (index >= 0) {
|
|
||||||
// Check if a static summary is set.
|
|
||||||
boolean isStaticSummary = listPref.getStaticSummary() != null;
|
|
||||||
if (!isStaticSummary) {
|
|
||||||
// Only update summary if it is not static.
|
|
||||||
CharSequence newSummary = listPref.getEntries()[index];
|
|
||||||
listPref.setSummary(newSummary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listPref.clearHighlightedEntriesForDialog();
|
|
||||||
searchItem.refreshHighlighting();
|
|
||||||
refreshSearchResults();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let subclasses handle special preferences.
|
|
||||||
setupSpecialPreferenceListeners(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects searchable preferences from a preference group.
|
|
||||||
*/
|
|
||||||
protected void collectSearchablePreferences(PreferenceGroup group) {
|
|
||||||
collectSearchablePreferencesWithKeys(group, "", new ArrayList<>(), 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects searchable preferences with their navigation paths and keys.
|
|
||||||
*
|
|
||||||
* @param group The preference group to collect from.
|
|
||||||
* @param parentPath The navigation path of the parent group.
|
|
||||||
* @param parentKeys The keys of parent preferences.
|
|
||||||
* @param includeDepth The maximum depth to include in the search index.
|
|
||||||
* @param currentDepth The current depth in the preference hierarchy.
|
|
||||||
*/
|
|
||||||
protected void collectSearchablePreferencesWithKeys(PreferenceGroup group, String parentPath,
|
|
||||||
List<String> parentKeys, int includeDepth, int currentDepth) {
|
|
||||||
if (group == null) return;
|
|
||||||
|
|
||||||
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
|
||||||
Preference preference = group.getPreference(i);
|
|
||||||
|
|
||||||
// Add to search results only if it is not a category, special group, or PreferenceScreen.
|
|
||||||
if (shouldIncludePreference(preference, currentDepth, includeDepth)) {
|
|
||||||
allSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(
|
|
||||||
preference, parentPath, parentKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the preference is a group, recurse into it.
|
|
||||||
if (preference instanceof PreferenceGroup subGroup) {
|
|
||||||
String newPath = parentPath;
|
|
||||||
List<String> newKeys = new ArrayList<>(parentKeys);
|
|
||||||
|
|
||||||
// Append the group title to the path and save key for navigation.
|
|
||||||
if (!isSpecialPreferenceGroup(preference)
|
|
||||||
&& !(preference instanceof NoTitlePreferenceCategory)) {
|
|
||||||
CharSequence title = preference.getTitle();
|
|
||||||
if (!TextUtils.isEmpty(title)) {
|
|
||||||
newPath = TextUtils.isEmpty(parentPath)
|
|
||||||
? title.toString()
|
|
||||||
: parentPath + " > " + title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add key for navigation if this is a PreferenceScreen or group with navigation capability.
|
|
||||||
String key = preference.getKey();
|
|
||||||
if (!TextUtils.isEmpty(key) && (preference instanceof PreferenceScreen
|
|
||||||
|| searchResultsAdapter.hasNavigationCapability(preference))) {
|
|
||||||
newKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collectSearchablePreferencesWithKeys(subGroup, newPath, newKeys, includeDepth, currentDepth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters all search items based on the provided query and displays results in the overlay.
|
|
||||||
* Applies highlighting to matching text and shows a "no results" message if nothing matches.
|
|
||||||
*/
|
|
||||||
protected void filterAndShowResults(String query) {
|
|
||||||
hideSearchHistory();
|
|
||||||
// Keep track of the previously displayed items to clear their highlights.
|
|
||||||
List<BaseSearchResultItem> previouslyDisplayedItems = new ArrayList<>(filteredSearchItems);
|
|
||||||
|
|
||||||
filteredSearchItems.clear();
|
|
||||||
|
|
||||||
String queryLower = Utils.normalizeTextToLowercase(query);
|
|
||||||
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
// Clear highlighting only for items that were previously visible.
|
|
||||||
// This avoids iterating through all items on every keystroke during filtering.
|
|
||||||
for (BaseSearchResultItem item : previouslyDisplayedItems) {
|
|
||||||
item.clearHighlighting();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect matched items first.
|
|
||||||
List<BaseSearchResultItem> matched = new ArrayList<>();
|
|
||||||
int matchCount = 0;
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
if (matchCount >= MAX_SEARCH_RESULTS) break; // Stop after collecting max results.
|
|
||||||
if (item.matchesQuery(queryLower)) {
|
|
||||||
item.applyHighlighting(queryPattern);
|
|
||||||
matched.add(item);
|
|
||||||
matchCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filteredSearchItems, inserting parent enablers for disabled dependents.
|
|
||||||
Set<String> addedParentKeys = new HashSet<>(2 * matched.size());
|
|
||||||
for (BaseSearchResultItem item : matched) {
|
|
||||||
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
|
||||||
String key = prefItem.preference.getKey();
|
|
||||||
Setting<?> setting = (key != null) ? Setting.getSettingFromPath(key) : null;
|
|
||||||
if (setting != null && !setting.isAvailable()) {
|
|
||||||
List<Setting<?>> parentSettings = setting.getParentSettings();
|
|
||||||
for (Setting<?> parentSetting : parentSettings) {
|
|
||||||
BaseSearchResultItem parentItem = keyToSearchItem.get(parentSetting.key);
|
|
||||||
if (parentItem != null && !addedParentKeys.contains(parentSetting.key)) {
|
|
||||||
if (!parentItem.matchesQuery(queryLower)) {
|
|
||||||
// Apply highlighting to parent items even if they don't match the query.
|
|
||||||
// This ensures they get their current effective summary calculated.
|
|
||||||
parentItem.applyHighlighting(queryPattern);
|
|
||||||
filteredSearchItems.add(parentItem);
|
|
||||||
}
|
|
||||||
addedParentKeys.add(parentSetting.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filteredSearchItems.add(item);
|
|
||||||
if (key != null) {
|
|
||||||
addedParentKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filteredSearchItems.isEmpty()) {
|
|
||||||
//noinspection ComparatorCombinators
|
|
||||||
Collections.sort(filteredSearchItems, (o1, o2) ->
|
|
||||||
o1.navigationPath.compareTo(o2.navigationPath)
|
|
||||||
);
|
|
||||||
List<BaseSearchResultItem> displayItems = new ArrayList<>();
|
|
||||||
String currentPath = null;
|
|
||||||
for (BaseSearchResultItem item : filteredSearchItems) {
|
|
||||||
if (!item.navigationPath.equals(currentPath)) {
|
|
||||||
BaseSearchResultItem header = new BaseSearchResultItem.GroupHeaderItem(item.navigationPath, item.navigationKeys);
|
|
||||||
displayItems.add(header);
|
|
||||||
currentPath = item.navigationPath;
|
|
||||||
}
|
|
||||||
displayItems.add(item);
|
|
||||||
}
|
|
||||||
filteredSearchItems.clear();
|
|
||||||
filteredSearchItems.addAll(displayItems);
|
|
||||||
}
|
|
||||||
// Show "No results found" if search results are empty.
|
|
||||||
if (filteredSearchItems.isEmpty()) {
|
|
||||||
Preference noResultsPreference = new Preference(activity);
|
|
||||||
noResultsPreference.setKey("no_results_placeholder");
|
|
||||||
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
|
||||||
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
|
||||||
noResultsPreference.setSelectable(false);
|
|
||||||
noResultsPreference.setIcon(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
|
||||||
filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResultsAdapter.notifyDataSetChanged();
|
|
||||||
overlayContainer.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the search interface by showing the search view and hiding the menu item.
|
|
||||||
* Configures the UI for search mode, shows the keyboard, and displays search suggestions.
|
|
||||||
*/
|
|
||||||
protected void openSearch() {
|
|
||||||
isSearchActive = true;
|
|
||||||
toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(false);
|
|
||||||
toolbar.setTitle("");
|
|
||||||
searchContainer.setVisibility(View.VISIBLE);
|
|
||||||
searchView.requestFocus();
|
|
||||||
// Configure soft input mode to adjust layout and show keyboard.
|
|
||||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
|
|
||||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
|
||||||
inputMethodManager.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
|
|
||||||
// Always show search history when opening search.
|
|
||||||
showSearchHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the search interface and restores the normal UI state.
|
|
||||||
* Hides the overlay, clears search results, dismisses the keyboard, and removes highlighting.
|
|
||||||
*/
|
|
||||||
public void closeSearch() {
|
|
||||||
isSearchActive = false;
|
|
||||||
isShowingSearchHistory = false;
|
|
||||||
|
|
||||||
searchHistoryManager.hideSearchHistoryContainer();
|
|
||||||
overlayContainer.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
filteredSearchItems.clear();
|
|
||||||
|
|
||||||
searchContainer.setVisibility(View.GONE);
|
|
||||||
toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(true);
|
|
||||||
toolbar.setTitle(originalTitle);
|
|
||||||
searchView.setQuery("", false);
|
|
||||||
// Hide keyboard and reset soft input mode.
|
|
||||||
inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
|
||||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
|
|
||||||
// Clear highlighting for all search items.
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
item.clearHighlighting();
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResultsAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the search history if enabled.
|
|
||||||
*/
|
|
||||||
protected void showSearchHistory() {
|
|
||||||
if (searchHistoryManager.isSearchHistoryEnabled()) {
|
|
||||||
overlayContainer.setVisibility(View.VISIBLE);
|
|
||||||
searchHistoryManager.showSearchHistory();
|
|
||||||
isShowingSearchHistory = true;
|
|
||||||
} else {
|
|
||||||
hideAllOverlays();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the search history container.
|
|
||||||
*/
|
|
||||||
protected void hideSearchHistory() {
|
|
||||||
searchHistoryManager.hideSearchHistoryContainer();
|
|
||||||
isShowingSearchHistory = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides all overlay containers, including search results and history.
|
|
||||||
*/
|
|
||||||
protected void hideAllOverlays() {
|
|
||||||
hideSearchHistory();
|
|
||||||
hideSearchResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the search results overlay and clears the filtered results.
|
|
||||||
*/
|
|
||||||
protected void hideSearchResults() {
|
|
||||||
overlayContainer.setVisibility(View.GONE);
|
|
||||||
filteredSearchItems.clear();
|
|
||||||
searchResultsAdapter.notifyDataSetChanged();
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
item.clearHighlighting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the search results display if the search is active and history is not shown.
|
|
||||||
*/
|
|
||||||
protected void refreshSearchResults() {
|
|
||||||
if (isSearchActive && !isShowingSearchHistory) {
|
|
||||||
searchResultsAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a search item corresponding to the given preference.
|
|
||||||
*
|
|
||||||
* @param preference The preference to find a search item for.
|
|
||||||
* @return The corresponding PreferenceSearchItem, or null if not found.
|
|
||||||
*/
|
|
||||||
public BaseSearchResultItem.PreferenceSearchItem findSearchItemByPreference(Preference preference) {
|
|
||||||
// First, search in filtered results.
|
|
||||||
for (BaseSearchResultItem item : filteredSearchItems) {
|
|
||||||
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
|
||||||
if (prefItem.preference == preference) {
|
|
||||||
return prefItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If not found, search in all items.
|
|
||||||
for (BaseSearchResultItem item : allSearchItems) {
|
|
||||||
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
|
||||||
if (prefItem.preference == preference) {
|
|
||||||
return prefItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the background color for search view components based on current theme.
|
|
||||||
*/
|
|
||||||
@ColorInt
|
|
||||||
public static int getSearchViewBackground() {
|
|
||||||
return Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), Utils.isDarkModeEnabled() ? 1.11f : 0.95f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a rounded background drawable for the main search view.
|
|
||||||
*/
|
|
||||||
protected static GradientDrawable createBackgroundDrawable() {
|
|
||||||
GradientDrawable background = new GradientDrawable();
|
|
||||||
background.setShape(GradientDrawable.RECTANGLE);
|
|
||||||
background.setCornerRadius(Dim.dp28);
|
|
||||||
background.setColor(getSearchViewBackground());
|
|
||||||
return background;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return if a search is currently active.
|
|
||||||
*/
|
|
||||||
public boolean isSearchActive() {
|
|
||||||
return isSearchActive;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.search;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_ENTRIES;
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_HISTORY;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.preference.BulletPointPreference;
|
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager for search history functionality.
|
|
||||||
*/
|
|
||||||
public class SearchHistoryManager {
|
|
||||||
/**
|
|
||||||
* Interface for handling history item selection.
|
|
||||||
*/
|
|
||||||
private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
|
|
||||||
|
|
||||||
private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
|
|
||||||
"clear_history_button", "id");
|
|
||||||
private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
|
|
||||||
"history_text", "id");
|
|
||||||
private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
|
|
||||||
"delete_icon", "id");
|
|
||||||
private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
|
|
||||||
"empty_history_title", "id");
|
|
||||||
private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
|
|
||||||
"empty_history_summary", "id");
|
|
||||||
private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
|
|
||||||
"search_history_header", "id");
|
|
||||||
private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
|
|
||||||
"revanced_settings_search_tips_summary", "id");
|
|
||||||
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
|
|
||||||
"revanced_preference_search_history_screen", "layout");
|
|
||||||
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
|
|
||||||
"revanced_preference_search_history_item", "layout");
|
|
||||||
private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
|
|
||||||
"search_history_list", "id");
|
|
||||||
|
|
||||||
private final Deque<String> searchHistory;
|
|
||||||
private final Activity activity;
|
|
||||||
private final SearchHistoryAdapter searchHistoryAdapter;
|
|
||||||
private final boolean showSettingsSearchHistory;
|
|
||||||
private final FrameLayout searchHistoryContainer;
|
|
||||||
|
|
||||||
public interface OnSelectHistoryItemListener {
|
|
||||||
void onSelectHistoryItem(String query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for SearchHistoryManager.
|
|
||||||
*
|
|
||||||
* @param activity The parent activity.
|
|
||||||
* @param overlayContainer The overlay container to hold the search history container.
|
|
||||||
* @param onSelectHistoryItemAction Callback for when a history item is selected.
|
|
||||||
*/
|
|
||||||
SearchHistoryManager(Activity activity, FrameLayout overlayContainer,
|
|
||||||
OnSelectHistoryItemListener onSelectHistoryItemAction) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.showSettingsSearchHistory = SETTINGS_SEARCH_HISTORY.get();
|
|
||||||
this.searchHistory = new LinkedList<>();
|
|
||||||
|
|
||||||
// Initialize search history from settings.
|
|
||||||
if (showSettingsSearchHistory) {
|
|
||||||
String entries = SETTINGS_SEARCH_ENTRIES.get();
|
|
||||||
if (!entries.isBlank()) {
|
|
||||||
searchHistory.addAll(Arrays.asList(entries.split("\n")));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Clear old saved history if the feature is disabled.
|
|
||||||
SETTINGS_SEARCH_ENTRIES.resetToDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create search history container.
|
|
||||||
this.searchHistoryContainer = new FrameLayout(activity);
|
|
||||||
searchHistoryContainer.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// Inflate search history layout.
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
|
||||||
View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN, searchHistoryContainer, false);
|
|
||||||
searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT));
|
|
||||||
|
|
||||||
// Add history container to overlay.
|
|
||||||
FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT);
|
|
||||||
overlayParams.gravity = Gravity.TOP;
|
|
||||||
overlayContainer.addView(searchHistoryContainer, overlayParams);
|
|
||||||
|
|
||||||
// Find the LinearLayout for the history list within the container.
|
|
||||||
LinearLayout searchHistoryListView = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
|
|
||||||
if (searchHistoryListView == null) {
|
|
||||||
throw new IllegalStateException("Search history list view not found in container");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up history adapter. Use a copy of the search history.
|
|
||||||
this.searchHistoryAdapter = new SearchHistoryAdapter(activity, searchHistoryListView,
|
|
||||||
new ArrayList<>(searchHistory), onSelectHistoryItemAction);
|
|
||||||
|
|
||||||
// Set up clear history button.
|
|
||||||
TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
|
|
||||||
clearHistoryButton.setOnClickListener(v -> createAndShowDialog(
|
|
||||||
str("revanced_settings_search_clear_history"),
|
|
||||||
str("revanced_settings_search_clear_history_message"),
|
|
||||||
this::clearAllSearchHistory
|
|
||||||
));
|
|
||||||
|
|
||||||
// Set up search tips summary.
|
|
||||||
CharSequence text = BulletPointPreference.formatIntoBulletPoints(
|
|
||||||
str("revanced_settings_search_tips_summary"));
|
|
||||||
TextView tipsSummary = historyView.findViewById(ID_SEARCH_TIPS_SUMMARY);
|
|
||||||
tipsSummary.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows search history screen - either with history items or empty history message.
|
|
||||||
*/
|
|
||||||
public void showSearchHistory() {
|
|
||||||
if (!showSettingsSearchHistory) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all view elements.
|
|
||||||
TextView emptyHistoryTitle = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_TITLE);
|
|
||||||
TextView emptyHistorySummary = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_SUMMARY);
|
|
||||||
TextView historyHeader = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_HEADER);
|
|
||||||
LinearLayout historyList = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
|
|
||||||
TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
|
|
||||||
|
|
||||||
if (searchHistory.isEmpty()) {
|
|
||||||
// Show empty history state.
|
|
||||||
showEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
|
|
||||||
hideHistoryViews(historyHeader, historyList, clearHistoryButton);
|
|
||||||
} else {
|
|
||||||
// Show history list state.
|
|
||||||
hideEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
|
|
||||||
showHistoryViews(historyHeader, historyList, clearHistoryButton);
|
|
||||||
|
|
||||||
// Update adapter with current history.
|
|
||||||
searchHistoryAdapter.clear();
|
|
||||||
searchHistoryAdapter.addAll(searchHistory);
|
|
||||||
searchHistoryAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the search history container.
|
|
||||||
showSearchHistoryContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a search query to the history, maintaining the size limit.
|
|
||||||
*/
|
|
||||||
public void saveSearchQuery(String query) {
|
|
||||||
if (!showSettingsSearchHistory) return;
|
|
||||||
|
|
||||||
searchHistory.remove(query); // Remove if already exists to update position.
|
|
||||||
searchHistory.addFirst(query); // Add to the most recent.
|
|
||||||
|
|
||||||
// Remove extra old entries.
|
|
||||||
while (searchHistory.size() > MAX_HISTORY_SIZE) {
|
|
||||||
String last = searchHistory.removeLast();
|
|
||||||
Logger.printDebug(() -> "Removing search history query: " + last);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSearchHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the search history to shared preferences.
|
|
||||||
*/
|
|
||||||
protected void saveSearchHistory() {
|
|
||||||
Logger.printDebug(() -> "Saving search history: " + searchHistory);
|
|
||||||
SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a search query from the history.
|
|
||||||
*/
|
|
||||||
public void removeSearchQuery(String query) {
|
|
||||||
searchHistory.remove(query);
|
|
||||||
saveSearchHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all search history.
|
|
||||||
*/
|
|
||||||
public void clearAllSearchHistory() {
|
|
||||||
searchHistory.clear();
|
|
||||||
saveSearchHistory();
|
|
||||||
searchHistoryAdapter.clear();
|
|
||||||
searchHistoryAdapter.notifyDataSetChanged();
|
|
||||||
showSearchHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if search history feature is enabled.
|
|
||||||
*/
|
|
||||||
public boolean isSearchHistoryEnabled() {
|
|
||||||
return showSettingsSearchHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the search history container and overlay.
|
|
||||||
*/
|
|
||||||
public void showSearchHistoryContainer() {
|
|
||||||
searchHistoryContainer.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the search history container.
|
|
||||||
*/
|
|
||||||
public void hideSearchHistoryContainer() {
|
|
||||||
searchHistoryContainer.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to show empty history views.
|
|
||||||
*/
|
|
||||||
protected void showEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
|
|
||||||
emptyTitle.setVisibility(View.VISIBLE);
|
|
||||||
emptyTitle.setText(str("revanced_settings_search_empty_history_title"));
|
|
||||||
emptySummary.setVisibility(View.VISIBLE);
|
|
||||||
emptySummary.setText(str("revanced_settings_search_empty_history_summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to hide empty history views.
|
|
||||||
*/
|
|
||||||
protected void hideEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
|
|
||||||
emptyTitle.setVisibility(View.GONE);
|
|
||||||
emptySummary.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to show history list views.
|
|
||||||
*/
|
|
||||||
protected void showHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
|
|
||||||
header.setVisibility(View.VISIBLE);
|
|
||||||
list.setVisibility(View.VISIBLE);
|
|
||||||
clearButton.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to hide history list views.
|
|
||||||
*/
|
|
||||||
protected void hideHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
|
|
||||||
header.setVisibility(View.GONE);
|
|
||||||
list.setVisibility(View.GONE);
|
|
||||||
clearButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and shows a dialog with the specified title, message, and confirmation action.
|
|
||||||
*
|
|
||||||
* @param title The title of the dialog.
|
|
||||||
* @param message The message to display in the dialog.
|
|
||||||
* @param confirmAction The action to perform when the dialog is confirmed.
|
|
||||||
*/
|
|
||||||
protected void createAndShowDialog(String title, String message, Runnable confirmAction) {
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
|
||||||
activity,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
confirmAction,
|
|
||||||
() -> {},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
|
||||||
dialog.setCancelable(true);
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom adapter for search history items.
|
|
||||||
*/
|
|
||||||
protected class SearchHistoryAdapter {
|
|
||||||
protected final Collection<String> history;
|
|
||||||
protected final LayoutInflater inflater;
|
|
||||||
protected final LinearLayout container;
|
|
||||||
protected final OnSelectHistoryItemListener onSelectHistoryItemListener;
|
|
||||||
|
|
||||||
public SearchHistoryAdapter(Context context, LinearLayout container, Collection<String> history,
|
|
||||||
OnSelectHistoryItemListener listener) {
|
|
||||||
this.history = history;
|
|
||||||
this.inflater = LayoutInflater.from(context);
|
|
||||||
this.container = container;
|
|
||||||
this.onSelectHistoryItemListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the container with current history items.
|
|
||||||
*/
|
|
||||||
public void notifyDataSetChanged() {
|
|
||||||
container.removeAllViews();
|
|
||||||
for (String query : history) {
|
|
||||||
View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM, container, false);
|
|
||||||
|
|
||||||
TextView historyText = view.findViewById(ID_HISTORY_TEXT);
|
|
||||||
ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
|
|
||||||
|
|
||||||
historyText.setText(query);
|
|
||||||
|
|
||||||
// Set click listener for main item (select query).
|
|
||||||
view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(query));
|
|
||||||
|
|
||||||
// Set click listener for delete icon.
|
|
||||||
deleteIcon.setOnClickListener(v -> createAndShowDialog(
|
|
||||||
query,
|
|
||||||
str("revanced_settings_search_remove_message"),
|
|
||||||
() -> {
|
|
||||||
removeSearchQuery(query);
|
|
||||||
remove(query);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
container.addView(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all views from the container and history list.
|
|
||||||
*/
|
|
||||||
public void clear() {
|
|
||||||
history.clear();
|
|
||||||
container.removeAllViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds all provided history items to the container.
|
|
||||||
*/
|
|
||||||
public void addAll(Collection<String> items) {
|
|
||||||
history.addAll(items);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a query from the history and updates the container.
|
|
||||||
*/
|
|
||||||
public void remove(String query) {
|
|
||||||
history.remove(query);
|
|
||||||
if (history.isEmpty()) {
|
|
||||||
// If history is now empty, show the empty history state.
|
|
||||||
showSearchHistory();
|
|
||||||
} else {
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,6 @@ public enum ClientType {
|
|||||||
"132.0.6808.3",
|
"132.0.6808.3",
|
||||||
"1.61.48",
|
"1.61.48",
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
"Android VR 1.61"
|
"Android VR 1.61"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
@@ -51,36 +50,8 @@ public enum ClientType {
|
|||||||
"107.0.5284.2",
|
"107.0.5284.2",
|
||||||
"1.43.32",
|
"1.43.32",
|
||||||
ANDROID_VR_1_61_48.useAuth,
|
ANDROID_VR_1_61_48.useAuth,
|
||||||
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
|
|
||||||
"Android VR 1.43"
|
"Android VR 1.43"
|
||||||
),
|
),
|
||||||
/**
|
|
||||||
* Video not playable: Paid / Movie / Private / Age-restricted.
|
|
||||||
* Note: The 'Authorization' key must be excluded from the header.
|
|
||||||
*
|
|
||||||
* According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing,
|
|
||||||
* the GVS did not return a valid response:
|
|
||||||
* [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550).
|
|
||||||
*
|
|
||||||
* According to the latest commit in yt-dlp, the GVS returns a valid response
|
|
||||||
* even if the 'androidSdkVersion' field is missing:
|
|
||||||
* [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693).
|
|
||||||
*
|
|
||||||
* For some reason, PoToken is not required.
|
|
||||||
*/
|
|
||||||
ANDROID_NO_SDK(
|
|
||||||
3,
|
|
||||||
"ANDROID",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
"20.05.46",
|
|
||||||
"com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip",
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
"Android No SDK"
|
|
||||||
),
|
|
||||||
/**
|
/**
|
||||||
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
|
||||||
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
|
||||||
@@ -98,8 +69,7 @@ public enum ClientType {
|
|||||||
"132.0.6779.0",
|
"132.0.6779.0",
|
||||||
"23.47.101",
|
"23.47.101",
|
||||||
true,
|
true,
|
||||||
false,
|
"Android Creator"
|
||||||
"Android Studio"
|
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
* Internal YT client for an unreleased YT client. May stop working at any time.
|
* Internal YT client for an unreleased YT client. May stop working at any time.
|
||||||
@@ -113,7 +83,6 @@ public enum ClientType {
|
|||||||
"0.1",
|
"0.1",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
"visionOS"
|
"visionOS"
|
||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +107,6 @@ public enum ClientType {
|
|||||||
"19.22.3",
|
"19.22.3",
|
||||||
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
|
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
|
||||||
false,
|
false,
|
||||||
true,
|
|
||||||
"iPadOS"
|
"iPadOS"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -212,11 +180,6 @@ public enum ClientType {
|
|||||||
*/
|
*/
|
||||||
public final boolean useAuth;
|
public final boolean useAuth;
|
||||||
|
|
||||||
/**
|
|
||||||
* If the client supports multiple audio tracks.
|
|
||||||
*/
|
|
||||||
public final boolean supportsMultiAudioTracks;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Friendly name displayed in stats for nerds.
|
* Friendly name displayed in stats for nerds.
|
||||||
*/
|
*/
|
||||||
@@ -237,7 +200,6 @@ public enum ClientType {
|
|||||||
@NonNull String cronetVersion,
|
@NonNull String cronetVersion,
|
||||||
String clientVersion,
|
String clientVersion,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
boolean supportsMultiAudioTracks,
|
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
@@ -251,7 +213,6 @@ public enum ClientType {
|
|||||||
this.cronetVersion = cronetVersion;
|
this.cronetVersion = cronetVersion;
|
||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
|
|
||||||
Locale defaultLocale = Locale.getDefault();
|
Locale defaultLocale = Locale.getDefault();
|
||||||
@@ -277,7 +238,6 @@ public enum ClientType {
|
|||||||
String clientVersion,
|
String clientVersion,
|
||||||
String userAgent,
|
String userAgent,
|
||||||
boolean useAuth,
|
boolean useAuth,
|
||||||
boolean supportsMultiAudioTracks,
|
|
||||||
String friendlyName) {
|
String friendlyName) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.clientName = clientName;
|
this.clientName = clientName;
|
||||||
@@ -288,7 +248,6 @@ public enum ClientType {
|
|||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.useAuth = useAuth;
|
this.useAuth = useAuth;
|
||||||
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
|
|
||||||
this.friendlyName = friendlyName;
|
this.friendlyName = friendlyName;
|
||||||
this.packageName = null;
|
this.packageName = null;
|
||||||
this.androidSdkVersion = null;
|
this.androidSdkVersion = null;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.text.TextUtils;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -14,11 +13,11 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofVideoStreamsPatch {
|
public class SpoofVideoStreamsPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain used for internet connectivity verification.
|
* Domain used for internet connectivity verification.
|
||||||
* It has an empty response body and is only used to check for a 204 response code.
|
* It has an empty response body and is only used to check for a 204 response code.
|
||||||
@@ -39,12 +38,12 @@ public class SpoofVideoStreamsPatch {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile AppLanguage languageOverride;
|
private static volatile AppLanguage languageOverride;
|
||||||
|
|
||||||
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
|
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_61_48;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If this patch was included during patching.
|
* @return If this patch was included during patching.
|
||||||
*/
|
*/
|
||||||
public static boolean isPatchIncluded() {
|
private static boolean isPatchIncluded() {
|
||||||
return false; // Modified during patching.
|
return false; // Modified during patching.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,25 +53,21 @@ public class SpoofVideoStreamsPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param language Language override for non-authenticated requests.
|
* @param language Language override for non-authenticated requests. If this is null then
|
||||||
|
* {@link BaseSettings#SPOOF_VIDEO_STREAMS_LANGUAGE} is used.
|
||||||
*/
|
*/
|
||||||
public static void setLanguageOverride(@Nullable AppLanguage language) {
|
public static void setLanguageOverride(@Nullable AppLanguage language) {
|
||||||
languageOverride = language;
|
languageOverride = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setClientsToUse(List<ClientType> availableClients, ClientType client) {
|
public static void setPreferredClient(ClientType client) {
|
||||||
preferredClient = Objects.requireNonNull(client);
|
preferredClient = Objects.requireNonNull(client);
|
||||||
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ClientType getPreferredClient() {
|
|
||||||
return preferredClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
||||||
return isPatchIncluded()
|
return isPatchIncluded()
|
||||||
&& SPOOF_STREAMING_DATA
|
&& SPOOF_STREAMING_DATA
|
||||||
&& !preferredClient.supportsMultiAudioTracks;
|
&& preferredClient != ClientType.IPADOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,35 +95,6 @@ public class SpoofVideoStreamsPatch {
|
|||||||
return playerRequestUri;
|
return playerRequestUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* Blocks /get_watch requests by returning an unreachable URI.
|
|
||||||
* /att/get requests are used to obtain a PoToken challenge.
|
|
||||||
* See: <a href="https://github.com/FreeTubeApp/FreeTube/blob/4b7208430bc1032019a35a35eb7c8a84987ddbd7/src/botGuardScript.js#L15">botGuardScript.js#L15</a>
|
|
||||||
* <p>
|
|
||||||
* Since the Spoof streaming data patch was implemented because a valid PoToken cannot be obtained,
|
|
||||||
* Blocking /att/get requests are not a problem.
|
|
||||||
*/
|
|
||||||
public static String blockGetAttRequest(String originalUrlString) {
|
|
||||||
if (SPOOF_STREAMING_DATA) {
|
|
||||||
try {
|
|
||||||
var originalUri = Uri.parse(originalUrlString);
|
|
||||||
String path = originalUri.getPath();
|
|
||||||
|
|
||||||
if (path != null && path.contains("att/get")) {
|
|
||||||
Logger.printDebug(() -> "Blocking 'att/get' by returning internet connection check uri");
|
|
||||||
|
|
||||||
return INTERNET_CONNECTION_CHECK_URI_STRING;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "blockGetAttRequest failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalUrlString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -162,7 +128,7 @@ public class SpoofVideoStreamsPatch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* Only invoked when playing a livestream on an Apple client.
|
* Only invoked when playing a livestream on an iOS client.
|
||||||
*/
|
*/
|
||||||
public static boolean fixHLSCurrentTime(boolean original) {
|
public static boolean fixHLSCurrentTime(boolean original) {
|
||||||
if (!SPOOF_STREAMING_DATA) {
|
if (!SPOOF_STREAMING_DATA) {
|
||||||
@@ -171,14 +137,6 @@ public class SpoofVideoStreamsPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Injection point.
|
|
||||||
* Fix audio stuttering in YouTube Music.
|
|
||||||
*/
|
|
||||||
public static boolean disableSABR() {
|
|
||||||
return SPOOF_STREAMING_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* Turns off a feature flag that interferes with spoofing.
|
* Turns off a feature flag that interferes with spoofing.
|
||||||
@@ -320,4 +278,11 @@ public class SpoofVideoStreamsPatch {
|
|||||||
|
|
||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return BaseSettings.SPOOF_VIDEO_STREAMS.get() && !preferredClient.useAuth;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.revanced.extension.shared.spoof.requests;
|
package app.revanced.extension.shared.spoof.requests;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
|
|
||||||
@@ -39,9 +42,12 @@ final class PlayerRoutes {
|
|||||||
JSONObject context = new JSONObject();
|
JSONObject context = new JSONObject();
|
||||||
|
|
||||||
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
||||||
if (language == null) {
|
if (language == null || clientType == ANDROID_VR_1_43_32) {
|
||||||
// Force original audio has not overrode the language.
|
// Force original audio has not overrode the language.
|
||||||
language = AppLanguage.DEFAULT;
|
// Or if YT has fallen over to the last unauthenticated client (VR 1.43), then
|
||||||
|
// always use the app language because forcing an audio stream of specific languages
|
||||||
|
// can sometimes fail so it's better to try and load something rather than nothing.
|
||||||
|
language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
|
||||||
}
|
}
|
||||||
//noinspection ExtractMethodRecommender
|
//noinspection ExtractMethodRecommender
|
||||||
Locale streamLocale = language.getLocale();
|
Locale streamLocale = language.getLocale();
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package app.revanced.extension.shared.theme;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public abstract class BaseThemePatch {
|
|
||||||
// Background colors.
|
|
||||||
protected static final int BLACK_COLOR = Utils.getResourceColor("yt_black1");
|
|
||||||
protected static final int WHITE_COLOR = Utils.getResourceColor("yt_white1");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value matches any of the provided values.
|
|
||||||
*
|
|
||||||
* @param value The value to check.
|
|
||||||
* @param of The array of values to compare against.
|
|
||||||
* @return True if the value matches any of the provided values.
|
|
||||||
*/
|
|
||||||
protected static boolean anyEquals(int value, int... of) {
|
|
||||||
for (int v : of) {
|
|
||||||
if (value == v) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to process color values for Litho components.
|
|
||||||
*
|
|
||||||
* @param originalValue The original color value.
|
|
||||||
* @param darkValues Array of dark mode color values to match.
|
|
||||||
* @param lightValues Array of light mode color values to match.
|
|
||||||
* @return The new or original color value.
|
|
||||||
*/
|
|
||||||
protected static int processColorValue(int originalValue, int[] darkValues, @Nullable int[] lightValues) {
|
|
||||||
if (Utils.isDarkModeEnabled()) {
|
|
||||||
if (anyEquals(originalValue, darkValues)) {
|
|
||||||
return BLACK_COLOR;
|
|
||||||
}
|
|
||||||
} else if (lightValues != null && anyEquals(originalValue, lightValues)) {
|
|
||||||
return WHITE_COLOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.adjustColorBrightness;
|
|
||||||
import static app.revanced.extension.shared.Utils.getAppBackgroundColor;
|
|
||||||
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA;
|
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
|
|
||||||
public class ColorDot {
|
|
||||||
private static final int STROKE_WIDTH = Dim.dp(1.5f);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a circular drawable with a main fill and a stroke.
|
|
||||||
* Stroke adapts to dark/light theme and transparency, applied only when color is transparent or matches app background.
|
|
||||||
*/
|
|
||||||
public static GradientDrawable createColorDotDrawable(@ColorInt int color) {
|
|
||||||
final boolean isDarkTheme = isDarkModeEnabled();
|
|
||||||
final boolean isTransparent = Color.alpha(color) == 0;
|
|
||||||
final int opaqueColor = color | 0xFF000000;
|
|
||||||
final int appBackground = getAppBackgroundColor();
|
|
||||||
final int strokeColor;
|
|
||||||
final int strokeWidth;
|
|
||||||
|
|
||||||
// Determine stroke color.
|
|
||||||
if (isTransparent || (opaqueColor == appBackground)) {
|
|
||||||
final int baseColor = isTransparent ? appBackground : opaqueColor;
|
|
||||||
strokeColor = adjustColorBrightness(baseColor, isDarkTheme ? 1.2f : 0.8f);
|
|
||||||
strokeWidth = STROKE_WIDTH;
|
|
||||||
} else {
|
|
||||||
strokeColor = 0;
|
|
||||||
strokeWidth = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create circular drawable with conditional stroke.
|
|
||||||
GradientDrawable circle = new GradientDrawable();
|
|
||||||
circle.setShape(GradientDrawable.OVAL);
|
|
||||||
circle.setColor(color);
|
|
||||||
circle.setStroke(strokeWidth, strokeColor);
|
|
||||||
|
|
||||||
return circle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the color dot drawable to the target view.
|
|
||||||
*/
|
|
||||||
public static void applyColorDot(View targetView, @ColorInt int color, boolean enabled) {
|
|
||||||
if (targetView == null) return;
|
|
||||||
targetView.setBackground(createColorDotDrawable(color));
|
|
||||||
targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
|
|
||||||
if (!isDarkModeEnabled()) {
|
|
||||||
targetView.setClipToOutline(true);
|
|
||||||
targetView.setElevation(Dim.dp2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility class for creating a customizable dialog with a title, message or EditText, and up to three buttons (OK, Cancel, Neutral).
|
|
||||||
* The dialog supports themed colors, rounded corners, and dynamic button layout based on screen width. It is dismissible by default.
|
|
||||||
*/
|
|
||||||
public class CustomDialog {
|
|
||||||
private final Context context;
|
|
||||||
private final Dialog dialog;
|
|
||||||
private final LinearLayout mainLayout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
|
|
||||||
* The dialog's appearance adapts to the app's dark mode setting, with rounded corners and customizable button actions.
|
|
||||||
* Buttons adjust dynamically to their text content and are arranged in a single row if they fit within 80% of the
|
|
||||||
* screen width, with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
|
|
||||||
* If buttons do not fit, each is placed on a separate row, all aligned to the right.
|
|
||||||
*
|
|
||||||
* @param context Context used to create the dialog.
|
|
||||||
* @param title Title text of the dialog.
|
|
||||||
* @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
|
|
||||||
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
|
||||||
* @param okButtonText OK button text, or null to use the default "OK" string.
|
|
||||||
* @param onOkClick Action to perform when the OK button is clicked.
|
|
||||||
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
|
||||||
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
|
||||||
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
|
||||||
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
|
||||||
* @return The Dialog and its main LinearLayout container.
|
|
||||||
*/
|
|
||||||
public static Pair<Dialog, LinearLayout> create(Context context, CharSequence title, CharSequence message,
|
|
||||||
@Nullable EditText editText, CharSequence okButtonText,
|
|
||||||
Runnable onOkClick, Runnable onCancelClick,
|
|
||||||
@Nullable CharSequence neutralButtonText,
|
|
||||||
@Nullable Runnable onNeutralClick,
|
|
||||||
boolean dismissDialogOnNeutralClick) {
|
|
||||||
Logger.printDebug(() -> "Creating custom dialog with title: " + title);
|
|
||||||
CustomDialog customDialog = new CustomDialog(context, title, message, editText,
|
|
||||||
okButtonText, onOkClick, onCancelClick,
|
|
||||||
neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
|
|
||||||
return new Pair<>(customDialog.dialog, customDialog.mainLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a custom dialog with the specified parameters.
|
|
||||||
*
|
|
||||||
* @param context Context used to create the dialog.
|
|
||||||
* @param title Title text of the dialog.
|
|
||||||
* @param message Message text of the dialog, or null if replaced by EditText.
|
|
||||||
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
|
||||||
* @param okButtonText OK button text, or null to use the default "OK" string.
|
|
||||||
* @param onOkClick Action to perform when the OK button is clicked.
|
|
||||||
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
|
||||||
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
|
||||||
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
|
||||||
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
|
||||||
*/
|
|
||||||
private CustomDialog(Context context, CharSequence title, CharSequence message, @Nullable EditText editText,
|
|
||||||
CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
|
||||||
@Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick,
|
|
||||||
boolean dismissDialogOnNeutralClick) {
|
|
||||||
this.context = context;
|
|
||||||
this.dialog = new Dialog(context);
|
|
||||||
this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
|
||||||
|
|
||||||
// Create main layout.
|
|
||||||
mainLayout = createMainLayout();
|
|
||||||
addTitle(title);
|
|
||||||
addContent(message, editText);
|
|
||||||
addButtons(okButtonText, onOkClick, onCancelClick, neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
|
|
||||||
|
|
||||||
// Set dialog content and window attributes.
|
|
||||||
dialog.setContentView(mainLayout);
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window != null) {
|
|
||||||
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the main layout for the dialog with vertical orientation and rounded corners.
|
|
||||||
*
|
|
||||||
* @return The configured LinearLayout for the dialog.
|
|
||||||
*/
|
|
||||||
private LinearLayout createMainLayout() {
|
|
||||||
LinearLayout layout = new LinearLayout(context);
|
|
||||||
layout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
layout.setPadding(Dim.dp24, Dim.dp16, Dim.dp24, Dim.dp24);
|
|
||||||
|
|
||||||
// Set rounded rectangle background.
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(28), null, null));
|
|
||||||
// Dialog background.
|
|
||||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
|
||||||
layout.setBackground(background);
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a title to the dialog if provided.
|
|
||||||
*
|
|
||||||
* @param title The title text to display.
|
|
||||||
*/
|
|
||||||
private void addTitle(CharSequence title) {
|
|
||||||
if (TextUtils.isEmpty(title)) return;
|
|
||||||
|
|
||||||
TextView titleView = new TextView(context);
|
|
||||||
titleView.setText(title);
|
|
||||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
titleView.setTextSize(18);
|
|
||||||
titleView.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
titleView.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
params.setMargins(0, 0, 0, Dim.dp16);
|
|
||||||
titleView.setLayoutParams(params);
|
|
||||||
|
|
||||||
mainLayout.addView(titleView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a message or EditText to the dialog within a ScrollView.
|
|
||||||
*
|
|
||||||
* @param message The message text to display (supports Spanned for HTML), or null if replaced by EditText.
|
|
||||||
* @param editText The EditText to include, or null if no EditText is needed.
|
|
||||||
*/
|
|
||||||
private void addContent(CharSequence message, @Nullable EditText editText) {
|
|
||||||
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
|
||||||
if (message == null && editText == null) return;
|
|
||||||
|
|
||||||
ScrollView scrollView = new ScrollView(context);
|
|
||||||
// Disable the vertical scrollbar.
|
|
||||||
scrollView.setVerticalScrollBarEnabled(false);
|
|
||||||
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
|
||||||
|
|
||||||
LinearLayout contentContainer = new LinearLayout(context);
|
|
||||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
scrollView.addView(contentContainer);
|
|
||||||
|
|
||||||
// EditText (if provided).
|
|
||||||
if (editText != null) {
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(10), null, null));
|
|
||||||
background.getPaint().setColor(Utils.getEditTextBackground());
|
|
||||||
scrollView.setPadding(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8);
|
|
||||||
scrollView.setBackground(background);
|
|
||||||
scrollView.setClipToOutline(true);
|
|
||||||
|
|
||||||
// Remove EditText from its current parent, if any.
|
|
||||||
ViewGroup parent = (ViewGroup) editText.getParent();
|
|
||||||
if (parent != null) parent.removeView(editText);
|
|
||||||
// Style the EditText to match the dialog theme.
|
|
||||||
editText.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
editText.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
editText.setPadding(0, 0, 0, 0);
|
|
||||||
contentContainer.addView(editText, new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
// Message (if not replaced by EditText).
|
|
||||||
} else {
|
|
||||||
TextView messageView = new TextView(context);
|
|
||||||
// Supports Spanned (HTML).
|
|
||||||
messageView.setText(message);
|
|
||||||
messageView.setTextSize(16);
|
|
||||||
messageView.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
// Enable HTML link clicking if the message contains links.
|
|
||||||
if (message instanceof Spanned) {
|
|
||||||
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
contentContainer.addView(messageView, new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weight to take available space.
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
0,
|
|
||||||
1.0f);
|
|
||||||
scrollView.setLayoutParams(params);
|
|
||||||
// Add ScrollView to main layout only if content exist.
|
|
||||||
mainLayout.addView(scrollView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds buttons to the dialog, arranging them dynamically based on their widths.
|
|
||||||
*
|
|
||||||
* @param okButtonText OK button text, or null to use the default "OK" string.
|
|
||||||
* @param onOkClick Action for the OK button click.
|
|
||||||
* @param onCancelClick Action for the Cancel button click, or null if no Cancel button.
|
|
||||||
* @param neutralButtonText Neutral button text, or null if no Neutral button.
|
|
||||||
* @param onNeutralClick Action for the Neutral button click, or null if no Neutral button.
|
|
||||||
* @param dismissDialogOnNeutralClick If the dialog should dismiss on Neutral button click.
|
|
||||||
*/
|
|
||||||
private void addButtons(CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
|
||||||
@Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick,
|
|
||||||
boolean dismissDialogOnNeutralClick) {
|
|
||||||
// Button container.
|
|
||||||
LinearLayout buttonContainer = new LinearLayout(context);
|
|
||||||
buttonContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
buttonContainerParams.setMargins(0, Dim.dp16, 0, 0);
|
|
||||||
buttonContainer.setLayoutParams(buttonContainerParams);
|
|
||||||
|
|
||||||
List<Button> buttons = new ArrayList<>();
|
|
||||||
List<Integer> buttonWidths = new ArrayList<>();
|
|
||||||
|
|
||||||
// Create buttons in order: Neutral, Cancel, OK.
|
|
||||||
if (neutralButtonText != null && onNeutralClick != null) {
|
|
||||||
Button neutralButton = createButton(neutralButtonText, onNeutralClick, false, dismissDialogOnNeutralClick);
|
|
||||||
buttons.add(neutralButton);
|
|
||||||
buttonWidths.add(measureButtonWidth(neutralButton));
|
|
||||||
}
|
|
||||||
if (onCancelClick != null) {
|
|
||||||
Button cancelButton = createButton(context.getString(android.R.string.cancel), onCancelClick, false, true);
|
|
||||||
buttons.add(cancelButton);
|
|
||||||
buttonWidths.add(measureButtonWidth(cancelButton));
|
|
||||||
}
|
|
||||||
if (onOkClick != null) {
|
|
||||||
Button okButton = createButton(
|
|
||||||
okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
|
|
||||||
onOkClick, true, true);
|
|
||||||
buttons.add(okButton);
|
|
||||||
buttonWidths.add(measureButtonWidth(okButton));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle button layout.
|
|
||||||
layoutButtons(buttonContainer, buttons, buttonWidths);
|
|
||||||
mainLayout.addView(buttonContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a styled button with customizable text, click behavior, and appearance.
|
|
||||||
*
|
|
||||||
* @param text The button text to display.
|
|
||||||
* @param onClick The action to perform on button click.
|
|
||||||
* @param isOkButton If this is the OK button, which uses distinct styling.
|
|
||||||
* @param dismissDialog If the dialog should dismiss when the button is clicked.
|
|
||||||
* @return The created Button.
|
|
||||||
*/
|
|
||||||
private Button createButton(CharSequence text, Runnable onClick, boolean isOkButton, boolean dismissDialog) {
|
|
||||||
Button button = new Button(context, null, 0);
|
|
||||||
button.setText(text);
|
|
||||||
button.setTextSize(14);
|
|
||||||
button.setAllCaps(false);
|
|
||||||
button.setSingleLine(true);
|
|
||||||
button.setEllipsize(TextUtils.TruncateAt.END);
|
|
||||||
button.setGravity(Gravity.CENTER);
|
|
||||||
// Set internal padding.
|
|
||||||
button.setPadding(Dim.dp16, 0, Dim.dp16, 0);
|
|
||||||
|
|
||||||
// Background color for OK button (inversion).
|
|
||||||
// Background color for Cancel or Neutral buttons.
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(20), null, null));
|
|
||||||
background.getPaint().setColor(isOkButton
|
|
||||||
? Utils.getOkButtonBackgroundColor()
|
|
||||||
: Utils.getCancelOrNeutralButtonBackgroundColor());
|
|
||||||
button.setBackground(background);
|
|
||||||
|
|
||||||
button.setTextColor(Utils.isDarkModeEnabled()
|
|
||||||
? (isOkButton ? Color.BLACK : Color.WHITE)
|
|
||||||
: (isOkButton ? Color.WHITE : Color.BLACK));
|
|
||||||
|
|
||||||
button.setOnClickListener(v -> {
|
|
||||||
if (onClick != null) onClick.run();
|
|
||||||
if (dismissDialog) dialog.dismiss();
|
|
||||||
});
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the width of a button.
|
|
||||||
*/
|
|
||||||
private int measureButtonWidth(Button button) {
|
|
||||||
button.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
return button.getMeasuredWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arranges buttons in the dialog, either in a single row or multiple rows based on their total width.
|
|
||||||
*
|
|
||||||
* @param buttonContainer The container for the buttons.
|
|
||||||
* @param buttons The list of buttons to arrange.
|
|
||||||
* @param buttonWidths The measured widths of the buttons.
|
|
||||||
*/
|
|
||||||
private void layoutButtons(LinearLayout buttonContainer, List<Button> buttons, List<Integer> buttonWidths) {
|
|
||||||
if (buttons.isEmpty()) return;
|
|
||||||
|
|
||||||
// Check if buttons fit in one row.
|
|
||||||
int totalWidth = 0;
|
|
||||||
for (Integer width : buttonWidths) {
|
|
||||||
totalWidth += width;
|
|
||||||
}
|
|
||||||
if (buttonWidths.size() > 1) {
|
|
||||||
// Add margins for gaps.
|
|
||||||
totalWidth += (buttonWidths.size() - 1) * Dim.dp8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single button: stretch to full width.
|
|
||||||
if (buttons.size() == 1) {
|
|
||||||
layoutSingleButton(buttonContainer, buttons.get(0));
|
|
||||||
} else if (totalWidth <= Dim.pctWidth(80)) {
|
|
||||||
// Single row: Neutral, Cancel, OK.
|
|
||||||
layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
|
|
||||||
} else {
|
|
||||||
// Multiple rows: OK, Cancel, Neutral.
|
|
||||||
layoutButtonsInColumns(buttonContainer, buttons);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arranges a single button, stretching it to full width.
|
|
||||||
*
|
|
||||||
* @param buttonContainer The container for the button.
|
|
||||||
* @param button The button to arrange.
|
|
||||||
*/
|
|
||||||
private void layoutSingleButton(LinearLayout buttonContainer, Button button) {
|
|
||||||
LinearLayout singleContainer = new LinearLayout(context);
|
|
||||||
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
singleContainer.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
|
||||||
if (parent != null) parent.removeView(button);
|
|
||||||
|
|
||||||
button.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
Dim.dp36));
|
|
||||||
singleContainer.addView(button);
|
|
||||||
buttonContainer.addView(singleContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arranges buttons in a single horizontal row with proportional widths.
|
|
||||||
*
|
|
||||||
* @param buttonContainer The container for the buttons.
|
|
||||||
* @param buttons The list of buttons to arrange.
|
|
||||||
* @param buttonWidths The measured widths of the buttons.
|
|
||||||
*/
|
|
||||||
private void layoutButtonsInRow(LinearLayout buttonContainer, List<Button> buttons, List<Integer> buttonWidths) {
|
|
||||||
LinearLayout rowContainer = new LinearLayout(context);
|
|
||||||
rowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
rowContainer.setGravity(Gravity.CENTER);
|
|
||||||
rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
|
|
||||||
// Add all buttons with proportional weights and specific margins.
|
|
||||||
for (int i = 0; i < buttons.size(); i++) {
|
|
||||||
Button button = getButton(buttons, buttonWidths, i);
|
|
||||||
rowContainer.addView(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonContainer.addView(rowContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Button getButton(List<Button> buttons, List<Integer> buttonWidths, int i) {
|
|
||||||
Button button = buttons.get(i);
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
|
||||||
if (parent != null) parent.removeView(button);
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
0, Dim.dp36, buttonWidths.get(i));
|
|
||||||
|
|
||||||
// Set margins based on button type and combination.
|
|
||||||
if (buttons.size() == 2) {
|
|
||||||
// Neutral + OK or Cancel + OK.
|
|
||||||
params.setMargins(i == 0 ? 0 : Dim.dp4, 0, i == 0 ? Dim.dp4 : 0, 0);
|
|
||||||
} else if (buttons.size() == 3) {
|
|
||||||
// Neutral.
|
|
||||||
// Cancel.
|
|
||||||
// OK.
|
|
||||||
params.setMargins(i == 0 ? 0 : Dim.dp4, 0, i == 2 ? 0 : Dim.dp4, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setLayoutParams(params);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arranges buttons in separate rows, ordered OK, Cancel, Neutral.
|
|
||||||
*
|
|
||||||
* @param buttonContainer The container for the buttons.
|
|
||||||
* @param buttons The list of buttons to arrange.
|
|
||||||
*/
|
|
||||||
private void layoutButtonsInColumns(LinearLayout buttonContainer, List<Button> buttons) {
|
|
||||||
// Reorder: OK, Cancel, Neutral.
|
|
||||||
List<Button> reorderedButtons = new ArrayList<>();
|
|
||||||
if (buttons.size() == 3) {
|
|
||||||
reorderedButtons.add(buttons.get(2)); // OK
|
|
||||||
reorderedButtons.add(buttons.get(1)); // Cancel
|
|
||||||
reorderedButtons.add(buttons.get(0)); // Neutral
|
|
||||||
} else if (buttons.size() == 2) {
|
|
||||||
reorderedButtons.add(buttons.get(1)); // OK or Cancel
|
|
||||||
reorderedButtons.add(buttons.get(0)); // Neutral or Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < reorderedButtons.size(); i++) {
|
|
||||||
Button button = reorderedButtons.get(i);
|
|
||||||
LinearLayout singleContainer = new LinearLayout(context);
|
|
||||||
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
singleContainer.setGravity(Gravity.CENTER);
|
|
||||||
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
Dim.dp36));
|
|
||||||
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
|
||||||
if (parent != null) parent.removeView(button);
|
|
||||||
|
|
||||||
button.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
Dim.dp36));
|
|
||||||
singleContainer.addView(button);
|
|
||||||
buttonContainer.addView(singleContainer);
|
|
||||||
|
|
||||||
// Add a spacer between the buttons (except the last one).
|
|
||||||
if (i < reorderedButtons.size() - 1) {
|
|
||||||
View spacer = new View(context);
|
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
Dim.dp8);
|
|
||||||
spacer.setLayoutParams(spacerParams);
|
|
||||||
buttonContainer.addView(spacer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for converting design units (dp) and screen percentages to pixels.
|
|
||||||
*/
|
|
||||||
public final class Dim {
|
|
||||||
private Dim() {} // Prevent instantiation.
|
|
||||||
|
|
||||||
private static final DisplayMetrics METRICS = Resources.getSystem().getDisplayMetrics();
|
|
||||||
public static final int SCREEN_WIDTH = METRICS.widthPixels;
|
|
||||||
public static final int SCREEN_HEIGHT = METRICS.heightPixels;
|
|
||||||
|
|
||||||
// DP constants (density-independent pixels).
|
|
||||||
public static final int dp1 = dp(1);
|
|
||||||
public static final int dp2 = dp(2);
|
|
||||||
public static final int dp4 = dp(4);
|
|
||||||
public static final int dp6 = dp(6);
|
|
||||||
public static final int dp7 = dp(7);
|
|
||||||
public static final int dp8 = dp(8);
|
|
||||||
public static final int dp10 = dp(10);
|
|
||||||
public static final int dp12 = dp(12);
|
|
||||||
public static final int dp16 = dp(16);
|
|
||||||
public static final int dp20 = dp(20);
|
|
||||||
public static final int dp24 = dp(24);
|
|
||||||
public static final int dp28 = dp(28);
|
|
||||||
public static final int dp32 = dp(32);
|
|
||||||
public static final int dp36 = dp(36);
|
|
||||||
public static final int dp40 = dp(40);
|
|
||||||
public static final int dp48 = dp(48);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts dp (density-independent pixels) to actual device pixels.
|
|
||||||
* Uses Android's official TypedValue.applyDimension() for accurate rounding.
|
|
||||||
*
|
|
||||||
* @param dp The dp value to convert (supports float, e.g. 1.2f).
|
|
||||||
* @return The equivalent pixel value as int.
|
|
||||||
*/
|
|
||||||
public static int dp(float dp) {
|
|
||||||
return (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, dp, METRICS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a percentage of the screen height to pixels.
|
|
||||||
*
|
|
||||||
* @param percent The percentage (0–100).
|
|
||||||
* @return The pixel value corresponding to the percentage of screen height.
|
|
||||||
*/
|
|
||||||
public static int pctHeight(int percent) {
|
|
||||||
return (SCREEN_HEIGHT * percent) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a percentage of the screen width to pixels.
|
|
||||||
*
|
|
||||||
* @param percent The percentage (0–100).
|
|
||||||
* @return The pixel value corresponding to the percentage of screen width.
|
|
||||||
*/
|
|
||||||
public static int pctWidth(int percent) {
|
|
||||||
return (SCREEN_WIDTH * percent) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a percentage of the screen's portrait width (min side) to pixels.
|
|
||||||
*
|
|
||||||
* @param percent The percentage (0–100).
|
|
||||||
* @return The pixel value.
|
|
||||||
*/
|
|
||||||
public static int pctPortraitWidth(int percent) {
|
|
||||||
final int portraitWidth = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
||||||
return (int) (portraitWidth * (percent / 100.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an array of corner radii for a rounded rectangle.
|
|
||||||
* All corners use the same radius.
|
|
||||||
*
|
|
||||||
* @param dp radius in density-independent pixels
|
|
||||||
* @return array of 8 floats: [top-left-x, top-left-y, top-right-x, top-right-y, ...]
|
|
||||||
*/
|
|
||||||
public static float[] roundedCorners(float dp) {
|
|
||||||
final float r = dp(dp);
|
|
||||||
return new float[]{r, r, r, r, r, r, r, r};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,455 +0,0 @@
|
|||||||
package app.revanced.extension.shared.ui;
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.AnimatorListenerAdapter;
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.VelocityTracker;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewConfiguration;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Scroller;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility class for creating a bottom sheet dialog that slides up from the bottom of the screen.
|
|
||||||
* The dialog supports drag-to-dismiss functionality, animations, and nested scrolling for scrollable content.
|
|
||||||
*/
|
|
||||||
public class SheetBottomDialog {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a {@link SlideDialog} that slides up from the bottom of the screen with a specified content view.
|
|
||||||
* The dialog supports drag-to-dismiss functionality, allowing the user to drag it downward to close it,
|
|
||||||
* with proper handling of nested scrolling for scrollable content (e.g., {@link ListView}).
|
|
||||||
* It includes side margins, a top spacer for drag interaction, and can be dismissed by touching outside.
|
|
||||||
*
|
|
||||||
* @param context The context used to create the dialog.
|
|
||||||
* @param contentView The {@link View} to be displayed inside the dialog, such as a {@link LinearLayout}
|
|
||||||
* containing a {@link ListView}, buttons, or other UI elements.
|
|
||||||
* @param animationDuration The duration of the slide-in and slide-out animations in milliseconds.
|
|
||||||
* @return A configured {@link SlideDialog} instance ready to be shown.
|
|
||||||
* @throws IllegalArgumentException If contentView is null.
|
|
||||||
*/
|
|
||||||
public static SlideDialog createSlideDialog(@NonNull Context context, @NonNull View contentView, int animationDuration) {
|
|
||||||
SlideDialog dialog = new SlideDialog(context);
|
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
||||||
dialog.setCanceledOnTouchOutside(true);
|
|
||||||
dialog.setCancelable(true);
|
|
||||||
|
|
||||||
// Create wrapper layout for side margins.
|
|
||||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
|
||||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
|
|
||||||
// Create drag container.
|
|
||||||
DraggableLinearLayout dragContainer = new DraggableLinearLayout(context, animationDuration);
|
|
||||||
dragContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
dragContainer.setDialog(dialog);
|
|
||||||
|
|
||||||
// Add top spacer.
|
|
||||||
View spacer = new View(context);
|
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp40);
|
|
||||||
spacer.setLayoutParams(spacerParams);
|
|
||||||
spacer.setClickable(true);
|
|
||||||
dragContainer.addView(spacer);
|
|
||||||
|
|
||||||
// Add content view.
|
|
||||||
ViewGroup parent = (ViewGroup) contentView.getParent();
|
|
||||||
if (parent != null) parent.removeView(contentView);
|
|
||||||
dragContainer.addView(contentView);
|
|
||||||
|
|
||||||
// Add drag container to wrapper layout.
|
|
||||||
wrapperLayout.addView(dragContainer);
|
|
||||||
|
|
||||||
dialog.setContentView(wrapperLayout);
|
|
||||||
|
|
||||||
// Configure dialog window.
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window != null) {
|
|
||||||
Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 0, 100, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up animation on drag container.
|
|
||||||
dialog.setAnimView(dragContainer);
|
|
||||||
dialog.setAnimationDuration(animationDuration);
|
|
||||||
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a {@link DraggableLinearLayout} with a rounded background and a centered handle bar,
|
|
||||||
* styled for use as the main layout in a {@link SlideDialog}. The layout has vertical orientation,
|
|
||||||
* includes padding, and supports drag-to-dismiss functionality with proper handling of nested scrolling
|
|
||||||
* for scrollable content (e.g., {@link ListView}) or clickable elements (e.g., buttons, {@link android.widget.SeekBar}).
|
|
||||||
*
|
|
||||||
* @param context The context used to create the layout.
|
|
||||||
* @param backgroundColor The background color for the layout as an {@link Integer}, or null to use
|
|
||||||
* the default dialog background color.
|
|
||||||
* @return A configured {@link DraggableLinearLayout} with a handle bar and styled background.
|
|
||||||
*/
|
|
||||||
public static DraggableLinearLayout createMainLayout(@NonNull Context context, @Nullable Integer backgroundColor) {
|
|
||||||
DraggableLinearLayout mainLayout = new DraggableLinearLayout(context);
|
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
layoutParams.setMargins(Dim.dp8, 0, Dim.dp8, Dim.dp8);
|
|
||||||
mainLayout.setLayoutParams(layoutParams);
|
|
||||||
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(12), null, null));
|
|
||||||
int color = (backgroundColor != null) ? backgroundColor : Utils.getDialogBackgroundColor();
|
|
||||||
background.getPaint().setColor(color);
|
|
||||||
mainLayout.setBackground(background);
|
|
||||||
|
|
||||||
// Add handle bar.
|
|
||||||
LinearLayout handleContainer = new LinearLayout(context);
|
|
||||||
LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
containerParams.setMargins(0, Dim.dp8, 0, 0);
|
|
||||||
handleContainer.setLayoutParams(containerParams);
|
|
||||||
handleContainer.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
|
|
||||||
View handleBar = new View(context);
|
|
||||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Dim.roundedCorners(4), null, null));
|
|
||||||
handleBackground.getPaint().setColor(Utils.adjustColorBrightness(color, 0.9f, 1.25f));
|
|
||||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(Dim.dp40, Dim.dp4);
|
|
||||||
handleBar.setLayoutParams(handleParams);
|
|
||||||
handleBar.setBackground(handleBackground);
|
|
||||||
|
|
||||||
handleContainer.addView(handleBar);
|
|
||||||
mainLayout.addView(handleContainer);
|
|
||||||
|
|
||||||
return mainLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom {@link LinearLayout} that provides drag-to-dismiss functionality for a {@link SlideDialog}.
|
|
||||||
* This layout intercepts touch events to allow dragging the dialog downward to dismiss it when the
|
|
||||||
* content cannot scroll upward. It ensures compatibility with scrollable content (e.g., {@link ListView},
|
|
||||||
* {@link ScrollView}) and clickable elements (e.g., buttons, {@link android.widget.SeekBar}) by prioritizing
|
|
||||||
* their touch events to prevent conflicts.
|
|
||||||
*
|
|
||||||
* <p>Dragging is enabled only after the dialog's slide-in animation completes. The dialog is dismissed
|
|
||||||
* if dragged beyond 50% of its height or with a downward fling velocity exceeding 800 px/s.</p>
|
|
||||||
*/
|
|
||||||
public static class DraggableLinearLayout extends LinearLayout {
|
|
||||||
private static final int MIN_FLING_VELOCITY = 800; // px/s
|
|
||||||
private static final float DISMISS_HEIGHT_FRACTION = 0.5f; // 50% of height.
|
|
||||||
|
|
||||||
private float initialTouchRawY; // Raw Y on ACTION_DOWN.
|
|
||||||
private float dragOffset; // Current drag translation.
|
|
||||||
private boolean isDragging;
|
|
||||||
private boolean isDragEnabled;
|
|
||||||
|
|
||||||
private final int animationDuration;
|
|
||||||
private final Scroller scroller;
|
|
||||||
private final VelocityTracker velocityTracker;
|
|
||||||
private final Runnable settleRunnable;
|
|
||||||
|
|
||||||
private SlideDialog dialog;
|
|
||||||
private float dismissThreshold;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@link DraggableLinearLayout} with the specified context.
|
|
||||||
*/
|
|
||||||
public DraggableLinearLayout(@NonNull Context context) {
|
|
||||||
this(context, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@link DraggableLinearLayout} with the specified context and animation duration.
|
|
||||||
*
|
|
||||||
* @param context The context used to initialize the layout.
|
|
||||||
* @param animDuration The duration of the drag animation in milliseconds.
|
|
||||||
*/
|
|
||||||
public DraggableLinearLayout(@NonNull Context context, int animDuration) {
|
|
||||||
super(context);
|
|
||||||
scroller = new Scroller(context, new DecelerateInterpolator());
|
|
||||||
velocityTracker = VelocityTracker.obtain();
|
|
||||||
animationDuration = animDuration;
|
|
||||||
settleRunnable = this::runSettleAnimation;
|
|
||||||
|
|
||||||
setClickable(true);
|
|
||||||
|
|
||||||
// Enable drag only after slide-in animation finishes.
|
|
||||||
isDragEnabled = false;
|
|
||||||
postDelayed(() -> isDragEnabled = true, animationDuration + 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the {@link SlideDialog} associated with this layout for dismissal.
|
|
||||||
*/
|
|
||||||
public void setDialog(@NonNull SlideDialog dialog) {
|
|
||||||
this.dialog = dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the dismissal threshold when the layout's size changes.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
|
||||||
dismissThreshold = h * DISMISS_HEIGHT_FRACTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercepts touch events to initiate dragging when the content cannot scroll upward and the
|
|
||||||
* touch movement exceeds the system's touch slop.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
||||||
if (!isDragEnabled) return false;
|
|
||||||
|
|
||||||
switch (ev.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
initialTouchRawY = ev.getRawY();
|
|
||||||
isDragging = false;
|
|
||||||
scroller.forceFinished(true);
|
|
||||||
removeCallbacks(settleRunnable);
|
|
||||||
velocityTracker.clear();
|
|
||||||
velocityTracker.addMovement(ev);
|
|
||||||
dragOffset = getTranslationY();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
float dy = ev.getRawY() - initialTouchRawY;
|
|
||||||
if (dy > ViewConfiguration.get(getContext()).getScaledTouchSlop()
|
|
||||||
&& !canChildScrollUp()) {
|
|
||||||
isDragging = true;
|
|
||||||
return true; // Intercept touches for drag.
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch events to perform dragging or trigger dismissal/return animations based on
|
|
||||||
* drag distance or fling velocity.
|
|
||||||
*/
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent ev) {
|
|
||||||
if (!isDragEnabled) return super.onTouchEvent(ev);
|
|
||||||
velocityTracker.addMovement(ev);
|
|
||||||
|
|
||||||
switch (ev.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
if (isDragging) {
|
|
||||||
float deltaY = ev.getRawY() - initialTouchRawY;
|
|
||||||
dragOffset = Math.max(0, deltaY); // Prevent upward drag.
|
|
||||||
setTranslationY(dragOffset); // 1:1 following finger.
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
velocityTracker.computeCurrentVelocity(1000);
|
|
||||||
float velocityY = velocityTracker.getYVelocity();
|
|
||||||
|
|
||||||
if (dragOffset > dismissThreshold || velocityY > MIN_FLING_VELOCITY) {
|
|
||||||
startDismissAnimation();
|
|
||||||
} else {
|
|
||||||
startReturnAnimation();
|
|
||||||
}
|
|
||||||
isDragging = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Consume the touch event to prevent focus changes on child views.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an animation to dismiss the dialog by sliding it downward.
|
|
||||||
*/
|
|
||||||
private void startDismissAnimation() {
|
|
||||||
scroller.startScroll(0, (int) dragOffset,
|
|
||||||
0, getHeight() - (int) dragOffset, animationDuration);
|
|
||||||
post(settleRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an animation to return the dialog to its original position.
|
|
||||||
*/
|
|
||||||
private void startReturnAnimation() {
|
|
||||||
scroller.startScroll(0, (int) dragOffset,
|
|
||||||
0, -(int) dragOffset, animationDuration);
|
|
||||||
post(settleRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the settle animation, updating the layout's translation until the animation completes.
|
|
||||||
* Dismisses the dialog if the drag offset reaches the view's height.
|
|
||||||
*/
|
|
||||||
private void runSettleAnimation() {
|
|
||||||
if (scroller.computeScrollOffset()) {
|
|
||||||
dragOffset = scroller.getCurrY();
|
|
||||||
setTranslationY(dragOffset);
|
|
||||||
|
|
||||||
if (dragOffset >= getHeight() && dialog != null) {
|
|
||||||
dialog.dismiss();
|
|
||||||
scroller.forceFinished(true);
|
|
||||||
} else {
|
|
||||||
post(settleRunnable);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dragOffset = getTranslationY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if any child view can scroll upward, preventing drag if scrolling is possible.
|
|
||||||
*
|
|
||||||
* @return True if a child can scroll upward, false otherwise.
|
|
||||||
*/
|
|
||||||
private boolean canChildScrollUp() {
|
|
||||||
View target = findScrollableChild(this);
|
|
||||||
return target != null && target.canScrollVertically(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively searches for a scrollable child view within the given view group.
|
|
||||||
*
|
|
||||||
* @param group The view group to search.
|
|
||||||
* @return The scrollable child view, or null if none found.
|
|
||||||
*/
|
|
||||||
private View findScrollableChild(ViewGroup group) {
|
|
||||||
for (int i = 0; i < group.getChildCount(); i++) {
|
|
||||||
View child = group.getChildAt(i);
|
|
||||||
if (child.canScrollVertically(-1)) return child;
|
|
||||||
if (child instanceof ViewGroup) {
|
|
||||||
View scroll = findScrollableChild((ViewGroup) child);
|
|
||||||
if (scroll != null) return scroll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom dialog that slides up from the bottom of the screen with animation. It supports
|
|
||||||
* drag-to-dismiss functionality and ensures smooth dismissal animations without overlapping
|
|
||||||
* dismiss calls. The dialog animates a specified view during show and dismiss operations.
|
|
||||||
*/
|
|
||||||
public static class SlideDialog extends Dialog {
|
|
||||||
private View animView;
|
|
||||||
private boolean isDismissing = false;
|
|
||||||
private int duration;
|
|
||||||
private final int screenHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@link SlideDialog} with the specified context.
|
|
||||||
*/
|
|
||||||
public SlideDialog(@NonNull Context context) {
|
|
||||||
super(context);
|
|
||||||
screenHeight = context.getResources().getDisplayMetrics().heightPixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the view to animate during show and dismiss operations.
|
|
||||||
*/
|
|
||||||
public void setAnimView(@NonNull View view) {
|
|
||||||
this.animView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the duration of the slide-in and slide-out animations.
|
|
||||||
*/
|
|
||||||
public void setAnimationDuration(int duration) {
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the dialog with a slide-up animation for the animated view, if set.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
super.show();
|
|
||||||
if (animView == null) return;
|
|
||||||
|
|
||||||
animView.setTranslationY(screenHeight);
|
|
||||||
animView.animate()
|
|
||||||
.translationY(0)
|
|
||||||
.setDuration(duration)
|
|
||||||
.setListener(null)
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the dialog, triggering a dismissal animation.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void cancel() {
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dismisses the dialog with a slide-down animation for the animated view, if set.
|
|
||||||
* Ensures that dismissal is not triggered multiple times concurrently.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void dismiss() {
|
|
||||||
if (isDismissing) return;
|
|
||||||
isDismissing = true;
|
|
||||||
|
|
||||||
Window window = getWindow();
|
|
||||||
if (window == null) {
|
|
||||||
super.dismiss();
|
|
||||||
isDismissing = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
|
||||||
float startDim = params != null ? params.dimAmount : 0f;
|
|
||||||
|
|
||||||
// Animate dimming effect.
|
|
||||||
ValueAnimator dimAnimator = ValueAnimator.ofFloat(startDim, 0f);
|
|
||||||
dimAnimator.setDuration(duration);
|
|
||||||
dimAnimator.addUpdateListener(animation -> {
|
|
||||||
if (params != null) {
|
|
||||||
params.dimAmount = (float) animation.getAnimatedValue();
|
|
||||||
window.setAttributes(params);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (animView == null) {
|
|
||||||
dimAnimator.addListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
SlideDialog.super.dismiss();
|
|
||||||
isDismissing = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dimAnimator.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dimAnimator.start();
|
|
||||||
animView.animate()
|
|
||||||
.translationY(screenHeight)
|
|
||||||
.setDuration(duration)
|
|
||||||
.setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
SlideDialog.super.dismiss();
|
|
||||||
isDismissing = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFil
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Deprecated(forRemoval = true)
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class HideCreateButtonPatch {
|
public final class HideCreateButtonPatch {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
package app.revanced.extension.spotify.misc.privacy;
|
package app.revanced.extension.spotify.misc.privacy;
|
||||||
|
|
||||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class SanitizeSharingLinksPatch {
|
public final class SanitizeSharingLinksPatch {
|
||||||
|
|
||||||
private static final LinkSanitizer sanitizer = new LinkSanitizer(
|
/**
|
||||||
|
* Parameters that are considered undesirable and should be stripped away.
|
||||||
|
*/
|
||||||
|
private static final List<String> SHARE_PARAMETERS_TO_REMOVE = List.of(
|
||||||
"si", // Share tracking parameter.
|
"si", // Share tracking parameter.
|
||||||
"utm_source" // Share source, such as "copy-link".
|
"utm_source" // Share source, such as "copy-link".
|
||||||
);
|
);
|
||||||
@@ -13,7 +20,25 @@ public final class SanitizeSharingLinksPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitizeSharingLink(String url) {
|
public static String sanitizeUrl(String url) {
|
||||||
return sanitizer.sanitizeUrlString(url);
|
try {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
Uri.Builder builder = uri.buildUpon().clearQuery();
|
||||||
|
|
||||||
|
for (String paramName : uri.getQueryParameterNames()) {
|
||||||
|
if (!SHARE_PARAMETERS_TO_REMOVE.contains(paramName)) {
|
||||||
|
for (String value : uri.getQueryParameters(paramName)) {
|
||||||
|
builder.appendQueryParameter(paramName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String sanitizedUrl = builder.build().toString();
|
||||||
|
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
|
||||||
|
return sanitizedUrl;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
compileOnly(project(":extensions:strava:stub"))
|
|
||||||
compileOnly(libs.okhttp)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
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<Future<Response>> 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<Response> 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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.android.library)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user