mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-21 01:07:32 +01:00
chore: Merge branch dev
to main
(#597)
This commit is contained in:
commit
6c6c636a43
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,3 +1,17 @@
|
|||||||
|
# [1.7.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.7.0-dev.1...v1.7.0-dev.2) (2024-03-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - GmsCore:** Require ignoring battery optimizations ([#599](https://github.com/ReVanced/revanced-integrations/issues/599)) ([fd2a9d0](https://github.com/ReVanced/revanced-integrations/commit/fd2a9d0287599aaafa817987fd0815e4f0ae72b9))
|
||||||
|
|
||||||
|
# [1.7.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.6.0...v1.7.0-dev.1) (2024-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Alternative thumbnails:** Selectively enable for home / subscription / search ([#593](https://github.com/ReVanced/revanced-integrations/issues/593)) ([4c81e96](https://github.com/ReVanced/revanced-integrations/commit/4c81e96a74cfc49923238c4a294b59f36b5e6c36))
|
||||||
|
|
||||||
# [1.6.0](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0...v1.6.0) (2024-03-28)
|
# [1.6.0](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0...v1.6.0) (2024-03-28)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package app.revanced.integrations.shared;
|
package app.revanced.integrations.shared;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.PowerManager;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static app.revanced.integrations.shared.StringRef.str;
|
import static app.revanced.integrations.shared.StringRef.str;
|
||||||
|
|
||||||
@ -18,15 +21,13 @@ import static app.revanced.integrations.shared.StringRef.str;
|
|||||||
*/
|
*/
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
private static final String GMS_CORE_PACKAGE_NAME
|
private static final String GMS_CORE_PACKAGE_NAME
|
||||||
= getGmsCoreVendor() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
|
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||||
private static final String DONT_KILL_MY_APP_LINK
|
private static final String DONT_KILL_MY_APP_LINK
|
||||||
= "https://dontkillmyapp.com";
|
= "https://dontkillmyapp.com";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
|
||||||
= Uri.parse("content://" + getGmsCoreVendor() + ".android.gsf.gservices/prefix");
|
|
||||||
|
|
||||||
private static void open(String queryOrLink, String message) {
|
|
||||||
Utils.showToastLong(message);
|
|
||||||
|
|
||||||
|
private static void open(String queryOrLink) {
|
||||||
Intent intent;
|
Intent intent;
|
||||||
try {
|
try {
|
||||||
// Check if queryOrLink is a valid URL.
|
// Check if queryOrLink is a valid URL.
|
||||||
@ -40,48 +41,93 @@ public class GmsCoreSupport {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
Utils.getContext().startActivity(intent);
|
Utils.getContext().startActivity(intent);
|
||||||
|
|
||||||
// Gracefully exit, otherwise without Gms the app crashes and Android can nag the user.
|
// Gracefully exit, otherwise the broken app will continue to run.
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void showToastOrDialog(Context context, String toastMessageKey, String dialogMessageKey, String link) {
|
||||||
|
if (!(context instanceof Activity)) {
|
||||||
|
// Context is for the application and cannot show a dialog using it.
|
||||||
|
Utils.showToastLong(str(toastMessageKey));
|
||||||
|
open(link);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a delay to allow the activity to finish initializing.
|
||||||
|
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||||
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||||
|
.setTitle(str("gms_core_dialog_title"))
|
||||||
|
.setMessage(str(dialogMessageKey))
|
||||||
|
.setPositiveButton(str("gms_core_dialog_ok_button_text"), (dialog, id) -> {
|
||||||
|
open(link);
|
||||||
|
})
|
||||||
|
// Manually allow using the back button to dismiss the dialog with the back button,
|
||||||
|
// if troubleshooting and somehow the GmsCore verification checks always fail.
|
||||||
|
.setCancelable(true)
|
||||||
|
.show();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
public static void checkAvailability() {
|
public static void checkGmsCore(Context context) {
|
||||||
var context = Objects.requireNonNull(Utils.getContext());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.getPackageManager().getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
// Verify GmsCore is installed.
|
||||||
} catch (PackageManager.NameNotFoundException exception) {
|
try {
|
||||||
Logger.printInfo(() -> "GmsCore was not found", exception);
|
PackageManager manager = context.getPackageManager();
|
||||||
open(getGmsCoreDownload(), str("gms_core_not_installed_warning"));
|
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||||
return;
|
} catch (PackageManager.NameNotFoundException exception) {
|
||||||
}
|
Logger.printDebug(() -> "GmsCore was not found");
|
||||||
|
// Cannot show a dialog and must show a toast,
|
||||||
|
// because on some installations the app crashes before the dialog can display.
|
||||||
|
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
|
||||||
|
open(getGmsCoreDownload());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
// Check if GmsCore is whitelisted from battery optimizations.
|
||||||
if (client != null) return;
|
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||||
|
if (!powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME)) {
|
||||||
|
Logger.printDebug(() -> "GmsCore is not whitelisted from battery optimizations");
|
||||||
|
showToastOrDialog(context,
|
||||||
|
"gms_core_toast_not_whitelisted_message",
|
||||||
|
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
||||||
|
DONT_KILL_MY_APP_LINK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
// Check if GmsCore is running in the background.
|
||||||
open(DONT_KILL_MY_APP_LINK, str("gms_core_not_running_warning"));
|
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||||
|
if (client == null) {
|
||||||
|
Logger.printDebug(() -> "GmsCore is not running in the background");
|
||||||
|
showToastOrDialog(context,
|
||||||
|
"gms_core_toast_not_whitelisted_message",
|
||||||
|
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||||
|
DONT_KILL_MY_APP_LINK);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Could not check GmsCore background task", ex);
|
Logger.printException(() -> "checkGmsCore failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getGmsCoreDownload() {
|
private static String getGmsCoreDownload() {
|
||||||
final var vendor = getGmsCoreVendor();
|
final var vendorGroupId = getGmsCoreVendorGroupId();
|
||||||
//noinspection SwitchStatementWithTooFewBranches
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
switch (vendor) {
|
switch (vendorGroupId) {
|
||||||
case "app.revanced":
|
case "app.revanced":
|
||||||
return "https://github.com/revanced/gmscore/releases/latest";
|
return "https://github.com/revanced/gmscore/releases/latest";
|
||||||
default:
|
default:
|
||||||
return vendor + ".android.gms";
|
return vendorGroupId + ".android.gms";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
// Modified by a patch. Do not touch.
|
||||||
private static String getGmsCoreVendor() {
|
private static String getGmsCoreVendorGroupId() {
|
||||||
return "app.revanced";
|
return "app.revanced";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
package app.revanced.integrations.shared.settings;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.integrations.shared.Logger;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an Enum value is removed or changed, any saved or imported data using the
|
||||||
|
* non-existent value will be reverted to the default value
|
||||||
|
* (the event is logged, but no user error is displayed).
|
||||||
|
*
|
||||||
|
* All saved JSON text is converted to lowercase to keep the output less obnoxious.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class EnumSetting<T extends Enum> extends Setting<T> {
|
||||||
|
public EnumSetting(String key, T defaultValue) {
|
||||||
|
super(key, defaultValue);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, boolean rebootApp) {
|
||||||
|
super(key, defaultValue, rebootApp);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
|
||||||
|
super(key, defaultValue, rebootApp, includeWithImportExport);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, String userDialogMessage) {
|
||||||
|
super(key, defaultValue, userDialogMessage);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, Availability availability) {
|
||||||
|
super(key, defaultValue, availability);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
|
||||||
|
super(key, defaultValue, rebootApp, userDialogMessage);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) {
|
||||||
|
super(key, defaultValue, rebootApp, availability);
|
||||||
|
}
|
||||||
|
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
|
||||||
|
super(key, defaultValue, rebootApp, userDialogMessage, availability);
|
||||||
|
}
|
||||||
|
public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
|
||||||
|
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void load() {
|
||||||
|
value = preferences.getEnum(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||||
|
String enumName = json.getString(importExportKey);
|
||||||
|
try {
|
||||||
|
return getEnumFromString(enumName);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
// Info level to allow removing enum values in the future without showing any user errors.
|
||||||
|
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
|
||||||
|
// Use lowercase to keep the output less ugly.
|
||||||
|
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private T getEnumFromString(String enumName) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
for (Enum value : defaultValue.getClass().getEnumConstants()) {
|
||||||
|
if (value.name().equalsIgnoreCase(enumName)) {
|
||||||
|
// noinspection unchecked
|
||||||
|
return (T) value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setValueFromString(@NonNull String newValue) {
|
||||||
|
value = getEnumFromString(Objects.requireNonNull(newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(@NonNull T newValue) {
|
||||||
|
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
|
||||||
|
value = Objects.requireNonNull(newValue);
|
||||||
|
preferences.saveEnumAsString(key, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public T get() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
@ -324,6 +324,7 @@ public abstract class Setting<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
|
||||||
* @return the value stored using the import/export key. Do not set any values in this method.
|
* @return the value stored using the import/export key. Do not set any values in this method.
|
||||||
*/
|
*/
|
||||||
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
|
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
|
||||||
|
@ -32,17 +32,31 @@ public class SharedPrefCategory {
|
|||||||
|
|
||||||
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
|
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
|
||||||
Logger.printException(() -> "Found conflicting preference: " + key);
|
Logger.printException(() -> "Found conflicting preference: " + key);
|
||||||
preferences.edit().remove(key).apply();
|
removeKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
||||||
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
|
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any preference data type that has the specified key.
|
||||||
|
*/
|
||||||
|
public void removeKey(@NonNull String key) {
|
||||||
|
preferences.edit().remove(Objects.requireNonNull(key)).apply();
|
||||||
|
}
|
||||||
|
|
||||||
public void saveBoolean(@NonNull String key, boolean value) {
|
public void saveBoolean(@NonNull String key, boolean value) {
|
||||||
preferences.edit().putBoolean(key, value).apply();
|
preferences.edit().putBoolean(key, value).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value a NULL parameter removes the value from the preferences
|
||||||
|
*/
|
||||||
|
public void saveEnumAsString(@NonNull String key, @Nullable Enum value) {
|
||||||
|
saveObjectAsString(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param value a NULL parameter removes the value from the preferences
|
* @param value a NULL parameter removes the value from the preferences
|
||||||
*/
|
*/
|
||||||
@ -83,6 +97,28 @@ public class SharedPrefCategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T extends Enum> T getEnum(@NonNull String key, @NonNull T _default) {
|
||||||
|
Objects.requireNonNull(_default);
|
||||||
|
try {
|
||||||
|
String enumName = preferences.getString(key, null);
|
||||||
|
if (enumName != null) {
|
||||||
|
try {
|
||||||
|
// noinspection unchecked
|
||||||
|
return (T) Enum.valueOf(_default.getClass(), enumName);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
// Info level to allow removing enum values in the future without showing any user errors.
|
||||||
|
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName);
|
||||||
|
removeKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ClassCastException ex) {
|
||||||
|
// Value stored is a completely different type (should never happen).
|
||||||
|
removeConflictingPreferenceKeyValue(key);
|
||||||
|
}
|
||||||
|
return _default;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getBoolean(@NonNull String key, boolean _default) {
|
public boolean getBoolean(@NonNull String key, boolean _default) {
|
||||||
try {
|
try {
|
||||||
return preferences.getBoolean(key, _default);
|
return preferences.getBoolean(key, _default);
|
||||||
@ -100,17 +136,16 @@ public class SharedPrefCategory {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
return Integer.valueOf(value);
|
return Integer.valueOf(value);
|
||||||
}
|
}
|
||||||
return _default;
|
} catch (ClassCastException | NumberFormatException ex) {
|
||||||
} catch (ClassCastException ex) {
|
|
||||||
try {
|
try {
|
||||||
// Old data previously stored as primitive.
|
// Old data previously stored as primitive.
|
||||||
return preferences.getInt(key, _default);
|
return preferences.getInt(key, _default);
|
||||||
} catch (ClassCastException ex2) {
|
} catch (ClassCastException ex2) {
|
||||||
// Value stored is a completely different type (should never happen).
|
// Value stored is a completely different type (should never happen).
|
||||||
removeConflictingPreferenceKeyValue(key);
|
removeConflictingPreferenceKeyValue(key);
|
||||||
return _default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -120,15 +155,14 @@ public class SharedPrefCategory {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
return Long.valueOf(value);
|
return Long.valueOf(value);
|
||||||
}
|
}
|
||||||
return _default;
|
} catch (ClassCastException | NumberFormatException ex) {
|
||||||
} catch (ClassCastException ex) {
|
|
||||||
try {
|
try {
|
||||||
return preferences.getLong(key, _default);
|
return preferences.getLong(key, _default);
|
||||||
} catch (ClassCastException ex2) {
|
} catch (ClassCastException ex2) {
|
||||||
removeConflictingPreferenceKeyValue(key);
|
removeConflictingPreferenceKeyValue(key);
|
||||||
return _default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -138,15 +172,14 @@ public class SharedPrefCategory {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
return Float.valueOf(value);
|
return Float.valueOf(value);
|
||||||
}
|
}
|
||||||
return _default;
|
} catch (ClassCastException | NumberFormatException ex) {
|
||||||
} catch (ClassCastException ex) {
|
|
||||||
try {
|
try {
|
||||||
return preferences.getFloat(key, _default);
|
return preferences.getFloat(key, _default);
|
||||||
} catch (ClassCastException ex2) {
|
} catch (ClassCastException ex2) {
|
||||||
removeConflictingPreferenceKeyValue(key);
|
removeConflictingPreferenceKeyValue(key);
|
||||||
return _default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -5,9 +5,15 @@ import androidx.annotation.GuardedBy;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.integrations.shared.settings.EnumSetting;
|
||||||
|
import app.revanced.integrations.shared.settings.Setting;
|
||||||
import app.revanced.integrations.youtube.settings.Settings;
|
import app.revanced.integrations.youtube.settings.Settings;
|
||||||
import app.revanced.integrations.shared.Logger;
|
import app.revanced.integrations.shared.Logger;
|
||||||
import app.revanced.integrations.shared.Utils;
|
import app.revanced.integrations.shared.Utils;
|
||||||
|
import app.revanced.integrations.youtube.shared.NavigationBar;
|
||||||
|
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||||
|
|
||||||
import org.chromium.net.UrlRequest;
|
import org.chromium.net.UrlRequest;
|
||||||
import org.chromium.net.UrlResponseInfo;
|
import org.chromium.net.UrlResponseInfo;
|
||||||
import org.chromium.net.impl.CronetUrlRequest;
|
import org.chromium.net.impl.CronetUrlRequest;
|
||||||
@ -21,6 +27,12 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import static app.revanced.integrations.shared.StringRef.str;
|
import static app.revanced.integrations.shared.StringRef.str;
|
||||||
|
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_HOME;
|
||||||
|
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY;
|
||||||
|
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER;
|
||||||
|
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH;
|
||||||
|
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS;
|
||||||
|
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alternative YouTube thumbnails.
|
* Alternative YouTube thumbnails.
|
||||||
@ -39,16 +51,72 @@ import static app.revanced.integrations.shared.StringRef.str;
|
|||||||
* If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail
|
* If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail
|
||||||
* is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution,
|
* is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution,
|
||||||
* because a noticeable number of videos do not have hq720 and too much fail to load.
|
* because a noticeable number of videos do not have hq720 and too much fail to load.
|
||||||
* <p>
|
|
||||||
* Ideas for improvements:
|
|
||||||
* - Selectively allow using original thumbnails in some situations,
|
|
||||||
* such as videos subscription feed, watch history, or in search results.
|
|
||||||
* - Save to a temporary file the video id's verified to have alt thumbnails.
|
|
||||||
* This would speed up loading the watch history and users saved playlists.
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class AlternativeThumbnailsPatch {
|
public final class AlternativeThumbnailsPatch {
|
||||||
|
|
||||||
|
// These must be class declarations if declared here,
|
||||||
|
// otherwise the app will not load due to cyclic initialization errors.
|
||||||
|
public static final class DeArrowAvailability implements Setting.Availability {
|
||||||
|
public static boolean usingDeArrowAnywhere() {
|
||||||
|
return ALT_THUMBNAIL_HOME.get().useDeArrow
|
||||||
|
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow
|
||||||
|
|| ALT_THUMBNAIL_LIBRARY.get().useDeArrow
|
||||||
|
|| ALT_THUMBNAIL_PLAYER.get().useDeArrow
|
||||||
|
|| ALT_THUMBNAIL_SEARCH.get().useDeArrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return usingDeArrowAnywhere();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class StillImagesAvailability implements Setting.Availability {
|
||||||
|
public static boolean usingStillImagesAnywhere() {
|
||||||
|
return ALT_THUMBNAIL_HOME.get().useStillImages
|
||||||
|
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages
|
||||||
|
|| ALT_THUMBNAIL_LIBRARY.get().useStillImages
|
||||||
|
|| ALT_THUMBNAIL_PLAYER.get().useStillImages
|
||||||
|
|| ALT_THUMBNAIL_SEARCH.get().useStillImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return usingStillImagesAnywhere();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThumbnailOption {
|
||||||
|
ORIGINAL(false, false),
|
||||||
|
DEARROW(true, false),
|
||||||
|
DEARROW_STILL_IMAGES(true, true),
|
||||||
|
STILL_IMAGES(false, true);
|
||||||
|
|
||||||
|
final boolean useDeArrow;
|
||||||
|
final boolean useStillImages;
|
||||||
|
|
||||||
|
ThumbnailOption(boolean useDeArrow, boolean useStillImages) {
|
||||||
|
this.useDeArrow = useDeArrow;
|
||||||
|
this.useStillImages = useStillImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ThumbnailStillTime {
|
||||||
|
BEGINNING(1),
|
||||||
|
MIDDLE(2),
|
||||||
|
END(3);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url alt image number. Such as the 2 in 'hq720_2.jpg'
|
||||||
|
*/
|
||||||
|
final int altImageNumber;
|
||||||
|
|
||||||
|
ThumbnailStillTime(int altImageNumber) {
|
||||||
|
this.altImageNumber = altImageNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final Uri dearrowApiUri;
|
private static final Uri dearrowApiUri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,6 +134,11 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
*/
|
*/
|
||||||
private static volatile long timeToResumeDeArrowAPICalls;
|
private static volatile long timeToResumeDeArrowAPICalls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used only for debug logging.
|
||||||
|
*/
|
||||||
|
private static volatile EnumSetting<ThumbnailOption> currentOptionSetting;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
dearrowApiUri = validateSettings();
|
dearrowApiUri = validateSettings();
|
||||||
final int port = dearrowApiUri.getPort();
|
final int port = dearrowApiUri.getPort();
|
||||||
@ -78,13 +151,6 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
* Fix any bad imported data.
|
* Fix any bad imported data.
|
||||||
*/
|
*/
|
||||||
private static Uri validateSettings() {
|
private static Uri validateSettings() {
|
||||||
final int altThumbnailType = Settings.ALT_THUMBNAIL_STILLS_TIME.get();
|
|
||||||
if (altThumbnailType < 1 || altThumbnailType > 3) {
|
|
||||||
Utils.showToastLong("Invalid Alternative still thumbnail type: "
|
|
||||||
+ altThumbnailType + ". Using default");
|
|
||||||
Settings.ALT_THUMBNAIL_STILLS_TIME.resetToDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
||||||
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
||||||
String scheme = apiUri.getScheme();
|
String scheme = apiUri.getScheme();
|
||||||
@ -96,12 +162,21 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
return apiUri;
|
return apiUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean usingDeArrow() {
|
private static EnumSetting<ThumbnailOption> optionSettingForCurrentNavigation() {
|
||||||
return Settings.ALT_THUMBNAIL_DEARROW.get();
|
if (NavigationBar.isSearchBarActive()) { // Must check search first.
|
||||||
}
|
return ALT_THUMBNAIL_SEARCH;
|
||||||
|
}
|
||||||
private static boolean usingVideoStills() {
|
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||||
return Settings.ALT_THUMBNAIL_STILLS.get();
|
return ALT_THUMBNAIL_PLAYER;
|
||||||
|
}
|
||||||
|
if (NavigationButton.HOME.isSelected()) {
|
||||||
|
return ALT_THUMBNAIL_HOME;
|
||||||
|
}
|
||||||
|
if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) {
|
||||||
|
return ALT_THUMBNAIL_SUBSCRIPTIONS;
|
||||||
|
}
|
||||||
|
// A library tab variant is active.
|
||||||
|
return ALT_THUMBNAIL_LIBRARY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,9 +254,16 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
*/
|
*/
|
||||||
public static String overrideImageURL(String originalUrl) {
|
public static String overrideImageURL(String originalUrl) {
|
||||||
try {
|
try {
|
||||||
final boolean usingDeArrow = usingDeArrow();
|
EnumSetting<ThumbnailOption> optionSetting = optionSettingForCurrentNavigation();
|
||||||
final boolean usingVideoStills = usingVideoStills();
|
ThumbnailOption option = optionSetting.get();
|
||||||
if (!usingDeArrow && !usingVideoStills) {
|
if (BaseSettings.DEBUG.get()) {
|
||||||
|
if (currentOptionSetting != optionSetting) {
|
||||||
|
currentOptionSetting = optionSetting;
|
||||||
|
Logger.printDebug(() -> "Changed to setting: " + optionSetting.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == ThumbnailOption.ORIGINAL) {
|
||||||
return originalUrl;
|
return originalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,14 +282,14 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
|
|
||||||
String sanitizedReplacementUrl;
|
String sanitizedReplacementUrl;
|
||||||
final boolean includeTracking;
|
final boolean includeTracking;
|
||||||
if (usingDeArrow && canUseDeArrowAPI()) {
|
if (option.useDeArrow && canUseDeArrowAPI()) {
|
||||||
includeTracking = false; // Do not include view tracking parameters with API call.
|
includeTracking = false; // Do not include view tracking parameters with API call.
|
||||||
final String fallbackUrl = usingVideoStills
|
final String fallbackUrl = option.useStillImages
|
||||||
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
|
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
|
||||||
: decodedUrl.sanitizedUrl;
|
: decodedUrl.sanitizedUrl;
|
||||||
|
|
||||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
||||||
} else if (usingVideoStills) {
|
} else if (option.useStillImages) {
|
||||||
includeTracking = true; // Include view tracking parameters if present.
|
includeTracking = true; // Include view tracking parameters if present.
|
||||||
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
|
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
|
||||||
} else {
|
} else {
|
||||||
@ -240,7 +322,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
|
|
||||||
String url = responseInfo.getUrl();
|
String url = responseInfo.getUrl();
|
||||||
|
|
||||||
if (usingDeArrow() && urlIsDeArrow(url)) {
|
if (urlIsDeArrow(url)) {
|
||||||
Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
|
Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
|
||||||
if (statusCode == 304) {
|
if (statusCode == 304) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
|
||||||
@ -250,7 +332,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usingVideoStills() && statusCode == 404) {
|
if (statusCode == 404) {
|
||||||
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
||||||
// The video is:
|
// The video is:
|
||||||
// - live stream
|
// - live stream
|
||||||
@ -294,15 +376,13 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
@Nullable UrlResponseInfo responseInfo,
|
@Nullable UrlResponseInfo responseInfo,
|
||||||
IOException exception) {
|
IOException exception) {
|
||||||
try {
|
try {
|
||||||
if (usingDeArrow()) {
|
String url = ((CronetUrlRequest) request).getHookedUrl();
|
||||||
String url = ((CronetUrlRequest) request).getHookedUrl();
|
if (urlIsDeArrow(url)) {
|
||||||
if (urlIsDeArrow(url)) {
|
Logger.printDebug(() -> "handleCronetFailure, exception: " + exception);
|
||||||
Logger.printDebug(() -> "handleCronetFailure, exception: " + exception);
|
final int statusCode = (responseInfo != null)
|
||||||
final int statusCode = (responseInfo != null)
|
? responseInfo.getHttpStatusCode()
|
||||||
? responseInfo.getHttpStatusCode()
|
: 0;
|
||||||
: 0;
|
handleDeArrowError(url, statusCode);
|
||||||
handleDeArrowError(url, statusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Callback failure error", ex);
|
Logger.printException(() -> "Callback failure error", ex);
|
||||||
@ -332,13 +412,13 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
for (ThumbnailQuality quality : values()) {
|
for (ThumbnailQuality quality : values()) {
|
||||||
originalNameToEnum.put(quality.originalName, quality);
|
originalNameToEnum.put(quality.originalName, quality);
|
||||||
|
|
||||||
for (int i = 1; i <= 3; i++) {
|
for (ThumbnailStillTime time : ThumbnailStillTime.values()) {
|
||||||
// 'custom' thumbnails set by the content creator.
|
// 'custom' thumbnails set by the content creator.
|
||||||
// These show up in place of regular thumbnails
|
// These show up in place of regular thumbnails
|
||||||
// and seem to be limited to [1, 3] range.
|
// and seem to be limited to the same [1, 3] range as the still captures.
|
||||||
originalNameToEnum.put(quality.originalName + "_custom_" + i, quality);
|
originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality);
|
||||||
|
|
||||||
altNameToEnum.put(quality.altImageName + i, quality);
|
altNameToEnum.put(quality.altImageName + time.altImageNumber, quality);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,7 +478,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getAltImageNameToUse() {
|
String getAltImageNameToUse() {
|
||||||
return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get();
|
return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,12 +590,11 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
|
|
||||||
boolean imageFileFound;
|
boolean imageFileFound;
|
||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "Verifying image: " + imageUrl);
|
|
||||||
// This hooked code is running on a low priority thread, and it's slightly faster
|
// This hooked code is running on a low priority thread, and it's slightly faster
|
||||||
// to run the url connection thru the integrations thread pool which runs at the highest priority.
|
// to run the url connection thru the integrations thread pool which runs at the highest priority.
|
||||||
final long start = System.currentTimeMillis();
|
final long start = System.currentTimeMillis();
|
||||||
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
||||||
final int connectionTimeoutMillis = 5000;
|
final int connectionTimeoutMillis = 10000; // 10 seconds.
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
|
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
|
||||||
connection.setConnectTimeout(connectionTimeoutMillis);
|
connection.setConnectTimeout(connectionTimeoutMillis);
|
||||||
connection.setReadTimeout(connectionTimeoutMillis);
|
connection.setReadTimeout(connectionTimeoutMillis);
|
||||||
@ -533,7 +612,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).get();
|
}).get();
|
||||||
Logger.printDebug(() -> "Alt verification took: " + (System.currentTimeMillis() - start) + "ms");
|
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl);
|
||||||
} catch (ExecutionException | InterruptedException ex) {
|
} catch (ExecutionException | InterruptedException ex) {
|
||||||
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
|
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
|
||||||
imageFileFound = false;
|
imageFileFound = false;
|
||||||
@ -597,7 +676,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection SameParameterValue*/
|
/** @noinspection SameParameterValue */
|
||||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||||
// especially for new videos uploaded in the last hour.
|
// especially for new videos uploaded in the last hour.
|
||||||
|
@ -6,6 +6,7 @@ import android.text.Html;
|
|||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import app.revanced.integrations.shared.Logger;
|
import app.revanced.integrations.shared.Logger;
|
||||||
import app.revanced.integrations.shared.Utils;
|
import app.revanced.integrations.shared.Utils;
|
||||||
import app.revanced.integrations.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
import app.revanced.integrations.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
||||||
@ -115,10 +116,10 @@ public final class AnnouncementsPatch {
|
|||||||
.setTitle(finalTitle)
|
.setTitle(finalTitle)
|
||||||
.setMessage(finalMessage)
|
.setMessage(finalMessage)
|
||||||
.setIcon(finalLevel.icon)
|
.setIcon(finalLevel.icon)
|
||||||
.setPositiveButton("Ok", (dialog, which) -> {
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}).setNegativeButton("Dismiss", (dialog, which) -> {
|
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
})
|
})
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
|
@ -129,8 +129,8 @@ public final class AdsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (exceptions.matches(path))
|
if (exceptions.matches(path))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
package app.revanced.integrations.youtube.patches.components;
|
package app.revanced.integrations.youtube.patches.components;
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import app.revanced.integrations.youtube.settings.Settings;
|
import app.revanced.integrations.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
final class ButtonsFilter extends Filter {
|
final class ButtonsFilter extends Filter {
|
||||||
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml";
|
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml";
|
||||||
|
|
||||||
@ -89,8 +85,8 @@ final class ButtonsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// If the current matched group is the action bar group,
|
// If the current matched group is the action bar group,
|
||||||
// in case every filter group is enabled, hide the action bar.
|
// in case every filter group is enabled, hide the action bar.
|
||||||
if (matchedGroup == actionBarGroup) {
|
if (matchedGroup == actionBarGroup) {
|
||||||
|
@ -10,7 +10,6 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -42,9 +41,9 @@ final class CustomFilter extends Filter {
|
|||||||
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the parsed objects, or NULL if there was a parse error.
|
* @return the parsed objects
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
||||||
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
|
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
|
||||||
@ -147,8 +146,8 @@ final class CustomFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// All callbacks are custom filter groups.
|
// All callbacks are custom filter groups.
|
||||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||||
if (custom.startsWith && contentIndex != 0) {
|
if (custom.startsWith && contentIndex != 0) {
|
||||||
|
@ -256,8 +256,8 @@ final class KeywordContentFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import app.revanced.integrations.shared.Logger;
|
|||||||
import app.revanced.integrations.youtube.StringTrieSearch;
|
import app.revanced.integrations.youtube.StringTrieSearch;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public final class LayoutComponentsFilter extends Filter {
|
public final class LayoutComponentsFilter extends Filter {
|
||||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||||
private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch();
|
private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch();
|
||||||
@ -265,8 +264,8 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == searchResultVideo) {
|
if (matchedGroup == searchResultVideo) {
|
||||||
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
|
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||||
|
@ -383,7 +383,6 @@ abstract class Filter {
|
|||||||
*/
|
*/
|
||||||
final class DummyFilter extends Filter { }
|
final class DummyFilter extends Filter { }
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class LithoFilterPatch {
|
public final class LithoFilterPatch {
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package app.revanced.integrations.youtube.patches.components;
|
package app.revanced.integrations.youtube.patches.components;
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@ -29,7 +26,6 @@ import app.revanced.integrations.youtube.TrieSearch;
|
|||||||
*
|
*
|
||||||
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
|
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,6 +49,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||||
try {
|
try {
|
||||||
if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
|
if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
|
||||||
@ -84,8 +81,8 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
||||||
if (result.isFiltered()) {
|
if (result.isFiltered()) {
|
||||||
String matchedVideoId = findVideoId(protobufBufferArray);
|
String matchedVideoId = findVideoId(protobufBufferArray);
|
||||||
|
@ -2,11 +2,9 @@ package app.revanced.integrations.youtube.patches.components;
|
|||||||
|
|
||||||
import static app.revanced.integrations.shared.Utils.hideViewUnderCondition;
|
import static app.revanced.integrations.shared.Utils.hideViewUnderCondition;
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||||
|
|
||||||
@ -16,7 +14,6 @@ import app.revanced.integrations.youtube.shared.NavigationBar;
|
|||||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public final class ShortsFilter extends Filter {
|
public final class ShortsFilter extends Filter {
|
||||||
public static PivotBar pivotBar; // Set by patch.
|
public static PivotBar pivotBar; // Set by patch.
|
||||||
private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
||||||
|
@ -3,6 +3,10 @@ package app.revanced.integrations.youtube.settings;
|
|||||||
import app.revanced.integrations.shared.Logger;
|
import app.revanced.integrations.shared.Logger;
|
||||||
import app.revanced.integrations.shared.settings.*;
|
import app.revanced.integrations.shared.settings.*;
|
||||||
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
|
||||||
|
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability;
|
||||||
|
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
|
||||||
|
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
|
||||||
|
import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime;
|
||||||
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
|
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||||
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
|
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
|
||||||
@ -52,13 +56,16 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
|
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
|
||||||
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
|
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
|
||||||
// Layout
|
// Layout
|
||||||
public static final BooleanSetting ALT_THUMBNAIL_STILLS = new BooleanSetting("revanced_alt_thumbnail_stills", FALSE);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||||
public static final IntegerSetting ALT_THUMBNAIL_STILLS_TIME = new IntegerSetting("revanced_alt_thumbnail_stills_time", 2, parent(ALT_THUMBNAIL_STILLS));
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
||||||
public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, parent(ALT_THUMBNAIL_STILLS));
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_LIBRARY = new EnumSetting<>("revanced_alt_thumbnail_library", ThumbnailOption.ORIGINAL);
|
||||||
public static final BooleanSetting ALT_THUMBNAIL_DEARROW = new BooleanSetting("revanced_alt_thumbnail_dearrow", FALSE);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_PLAYER = new EnumSetting<>("revanced_alt_thumbnail_player", ThumbnailOption.ORIGINAL);
|
||||||
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SEARCH = new EnumSetting<>("revanced_alt_thumbnail_search", ThumbnailOption.ORIGINAL);
|
||||||
public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url",
|
public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url",
|
||||||
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parent(ALT_THUMBNAIL_DEARROW));
|
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, new DeArrowAvailability());
|
||||||
public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", TRUE, parent(ALT_THUMBNAIL_DEARROW));
|
public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", TRUE, new DeArrowAvailability());
|
||||||
|
public static final EnumSetting<ThumbnailStillTime> ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability());
|
||||||
|
public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability());
|
||||||
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
|
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
|
||||||
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
||||||
public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true);
|
public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true);
|
||||||
@ -365,7 +372,7 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
|
|
||||||
// Remove any previously saved announcement consumer (a random generated string).
|
// Remove any previously saved announcement consumer (a random generated string).
|
||||||
Setting.preferences.saveString("revanced_announcement_consumer", null);
|
Setting.preferences.removeKey("revanced_announcement_consumer");
|
||||||
|
|
||||||
// Shorts
|
// Shorts
|
||||||
if (DEPRECATED_HIDE_SHORTS.get()) {
|
if (DEPRECATED_HIDE_SHORTS.get()) {
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
package app.revanced.integrations.youtube.settings.preference;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import app.revanced.integrations.shared.Logger;
|
|
||||||
import app.revanced.integrations.shared.Utils;
|
|
||||||
import app.revanced.integrations.shared.settings.Setting;
|
|
||||||
import app.revanced.integrations.youtube.settings.Settings;
|
|
||||||
|
|
||||||
import static app.revanced.integrations.shared.StringRef.str;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows what thumbnails will be used based on the current settings.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class AlternativeThumbnailsStatusPreference extends Preference {
|
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
|
||||||
// Because this listener may run before the ReVanced settings fragment updates SettingsEnum,
|
|
||||||
// this could show the prior config and not the current.
|
|
||||||
//
|
|
||||||
// Push this call to the end of the main run queue,
|
|
||||||
// so all other listeners are done and SettingsEnum is up to date.
|
|
||||||
Utils.runOnMainThread(this::updateUI);
|
|
||||||
};
|
|
||||||
|
|
||||||
public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
public AlternativeThumbnailsStatusPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addChangeListener() {
|
|
||||||
Logger.printDebug(() -> "addChangeListener");
|
|
||||||
Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeChangeListener() {
|
|
||||||
Logger.printDebug(() -> "removeChangeListener");
|
|
||||||
Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
|
|
||||||
super.onAttachedToHierarchy(preferenceManager);
|
|
||||||
updateUI();
|
|
||||||
addChangeListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPrepareForRemoval() {
|
|
||||||
super.onPrepareForRemoval();
|
|
||||||
removeChangeListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
Logger.printDebug(() -> "updateUI");
|
|
||||||
final boolean usingDeArrow = Settings.ALT_THUMBNAIL_DEARROW.get();
|
|
||||||
final boolean usingVideoStills = Settings.ALT_THUMBNAIL_STILLS.get();
|
|
||||||
|
|
||||||
final String summaryTextKey;
|
|
||||||
if (usingDeArrow && usingVideoStills) {
|
|
||||||
summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow_stills";
|
|
||||||
} else if (usingDeArrow) {
|
|
||||||
summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow";
|
|
||||||
} else if (usingVideoStills) {
|
|
||||||
summaryTextKey = "revanced_alt_thumbnail_about_status_stills";
|
|
||||||
} else {
|
|
||||||
summaryTextKey = "revanced_alt_thumbnail_about_status_disabled";
|
|
||||||
}
|
|
||||||
|
|
||||||
setSummary(str(summaryTextKey));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +1,37 @@
|
|||||||
package app.revanced.integrations.youtube.shared;
|
package app.revanced.integrations.youtube.shared;
|
||||||
|
|
||||||
|
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
import app.revanced.integrations.shared.Logger;
|
import app.revanced.integrations.shared.Logger;
|
||||||
import app.revanced.integrations.shared.Utils;
|
import app.revanced.integrations.shared.Utils;
|
||||||
import app.revanced.integrations.youtube.settings.Settings;
|
import app.revanced.integrations.youtube.settings.Settings;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class NavigationBar {
|
public final class NavigationBar {
|
||||||
private static volatile boolean searchbarIsActive;
|
|
||||||
|
private static volatile WeakReference<View> searchBarResultsRef = new WeakReference<>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static void searchBarResultsViewLoaded(View searchbarResults) {
|
public static void searchBarResultsViewLoaded(View searchbarResults) {
|
||||||
searchbarResults.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
searchBarResultsRef = new WeakReference<>(searchbarResults);
|
||||||
final boolean isActive = searchbarResults.getParent() != null;
|
|
||||||
|
|
||||||
if (searchbarIsActive != isActive) {
|
|
||||||
searchbarIsActive = isActive;
|
|
||||||
Logger.printDebug(() -> "searchbarIsActive: " + isActive);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the search bar is on screen.
|
||||||
|
*/
|
||||||
public static boolean isSearchBarActive() {
|
public static boolean isSearchBarActive() {
|
||||||
return searchbarIsActive;
|
View searchbarResults = searchBarResultsRef.get();
|
||||||
|
return searchbarResults != null && searchbarResults.getParent() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ public final class NavigationBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection EmptyMethod*/
|
/** @noinspection EmptyMethod*/
|
||||||
private static void navigationTabCreatedCallback(NavigationBar.NavigationButton button, View tabView) {
|
private static void navigationTabCreatedCallback(NavigationButton button, View tabView) {
|
||||||
// Code is added during patching.
|
// Code is added during patching.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ public final class NavigationBar {
|
|||||||
* Notifications tab. Only present when
|
* Notifications tab. Only present when
|
||||||
* {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active.
|
* {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active.
|
||||||
*/
|
*/
|
||||||
ACTIVITY("TAB_ACTIVITY"),
|
NOTIFICATIONS("TAB_ACTIVITY"),
|
||||||
/**
|
/**
|
||||||
* Library tab when the user is not logged in.
|
* Library tab when the user is not logged in.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching = true
|
org.gradle.caching = true
|
||||||
android.useAndroidX = true
|
android.useAndroidX = true
|
||||||
version = 1.6.0
|
version = 1.7.0-dev.2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user