2023-09-20 05:33:02 +02:00
|
|
|
package app.revanced.patches.youtube.layout.thumbnails
|
2023-08-27 21:40:49 +02:00
|
|
|
|
|
|
|
import app.revanced.patcher.data.BytecodeContext
|
|
|
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
|
|
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
2023-12-11 02:07:09 +01:00
|
|
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
|
|
|
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
2023-08-27 21:40:49 +02:00
|
|
|
import app.revanced.patcher.patch.BytecodePatch
|
2023-09-20 05:33:02 +02:00
|
|
|
import app.revanced.patcher.patch.annotation.CompatiblePackage
|
|
|
|
import app.revanced.patcher.patch.annotation.Patch
|
2023-08-27 21:40:49 +02:00
|
|
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
2023-12-11 02:07:09 +01:00
|
|
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
2023-08-27 21:40:49 +02:00
|
|
|
import app.revanced.patches.shared.settings.preference.impl.*
|
2023-12-11 02:07:09 +01:00
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.MessageDigestImageUrlFingerprint
|
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.MessageDigestImageUrlParentFingerprint
|
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.RequestFingerprint
|
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnFailureFingerprint
|
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnResponseStartedFingerprint
|
|
|
|
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnSucceededFingerprint
|
2023-09-20 05:33:02 +02:00
|
|
|
import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch
|
|
|
|
import app.revanced.patches.youtube.misc.settings.SettingsPatch
|
2023-12-11 02:07:09 +01:00
|
|
|
import app.revanced.util.exception
|
|
|
|
import com.android.tools.smali.dexlib2.AccessFlags
|
|
|
|
import com.android.tools.smali.dexlib2.Opcode
|
|
|
|
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
|
|
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
|
|
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
|
|
|
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
2023-08-27 21:40:49 +02:00
|
|
|
|
2023-09-20 05:33:02 +02:00
|
|
|
@Patch(
|
|
|
|
name = "Alternative thumbnails",
|
|
|
|
description = "Adds options to replace video thumbnails with still image captures of the video.",
|
2023-12-11 02:07:09 +01:00
|
|
|
dependencies = [IntegrationsPatch::class, SettingsPatch::class, AlternativeThumbnailsResourcePatch::class],
|
2023-09-20 05:33:02 +02:00
|
|
|
compatiblePackages = [
|
|
|
|
CompatiblePackage(
|
|
|
|
"com.google.android.youtube",
|
|
|
|
[
|
2023-09-28 01:28:25 +02:00
|
|
|
"18.32.39",
|
2023-10-08 03:37:42 +02:00
|
|
|
"18.37.36",
|
2023-11-12 16:43:11 +01:00
|
|
|
"18.38.44",
|
2023-11-12 17:01:49 +01:00
|
|
|
"18.43.45",
|
|
|
|
"18.44.41",
|
2023-12-02 00:07:29 +01:00
|
|
|
"18.45.41",
|
|
|
|
"18.45.43"
|
2023-09-20 05:33:02 +02:00
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
@Suppress("unused")
|
|
|
|
object AlternativeThumbnailsPatch : BytecodePatch(
|
2023-12-11 02:07:09 +01:00
|
|
|
setOf(
|
|
|
|
MessageDigestImageUrlParentFingerprint,
|
|
|
|
OnResponseStartedFingerprint,
|
|
|
|
RequestFingerprint,
|
|
|
|
)
|
2023-08-27 21:40:49 +02:00
|
|
|
) {
|
2023-09-20 05:33:02 +02:00
|
|
|
private const val INTEGRATIONS_CLASS_DESCRIPTOR =
|
|
|
|
"Lapp/revanced/integrations/patches/AlternativeThumbnailsPatch;"
|
|
|
|
|
|
|
|
private lateinit var loadImageUrlMethod: MutableMethod
|
|
|
|
private var loadImageUrlIndex = 0
|
|
|
|
|
|
|
|
private lateinit var loadImageSuccessCallbackMethod: MutableMethod
|
|
|
|
private var loadImageSuccessCallbackIndex = 0
|
|
|
|
|
|
|
|
private lateinit var loadImageErrorCallbackMethod: MutableMethod
|
|
|
|
private var loadImageErrorCallbackIndex = 0
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param highPriority If the hook should be called before all other hooks.
|
|
|
|
*/
|
2023-12-11 02:07:09 +01:00
|
|
|
@Suppress("SameParameterValue")
|
2023-09-20 05:33:02 +02:00
|
|
|
private fun addImageUrlHook(targetMethodClass: String, highPriority: Boolean) {
|
|
|
|
loadImageUrlMethod.addInstructions(
|
2023-12-11 02:07:09 +01:00
|
|
|
if (highPriority) 0 else loadImageUrlIndex,
|
|
|
|
"""
|
|
|
|
invoke-static { p1 }, $targetMethodClass->overrideImageURL(Ljava/lang/String;)Ljava/lang/String;
|
|
|
|
move-result-object p1
|
2023-09-20 05:33:02 +02:00
|
|
|
"""
|
|
|
|
)
|
|
|
|
loadImageUrlIndex += 2
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If a connection completed, which includes normal 200 responses but also includes
|
|
|
|
* status 404 and other error like http responses.
|
|
|
|
*/
|
2023-12-11 02:07:09 +01:00
|
|
|
@Suppress("SameParameterValue")
|
2023-09-20 05:33:02 +02:00
|
|
|
private fun addImageUrlSuccessCallbackHook(targetMethodClass: String) {
|
|
|
|
loadImageSuccessCallbackMethod.addInstruction(
|
|
|
|
loadImageSuccessCallbackIndex++,
|
2023-12-11 02:07:09 +01:00
|
|
|
"invoke-static { p1, p2 }, $targetMethodClass->handleCronetSuccess(" +
|
|
|
|
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V"
|
2023-09-20 05:33:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If a connection outright failed to complete any connection.
|
|
|
|
*/
|
2023-12-11 02:07:09 +01:00
|
|
|
@Suppress("SameParameterValue")
|
|
|
|
private fun addImageUrlErrorCallbackHook(targetMethodClass: String) {
|
2023-09-20 05:33:02 +02:00
|
|
|
loadImageErrorCallbackMethod.addInstruction(
|
|
|
|
loadImageErrorCallbackIndex++,
|
2023-12-11 02:07:09 +01:00
|
|
|
"invoke-static { p1, p2, p3 }, $targetMethodClass->handleCronetFailure(" +
|
|
|
|
"Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/io/IOException;)V"
|
2023-09-20 05:33:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:40:49 +02:00
|
|
|
override fun execute(context: BytecodeContext) {
|
|
|
|
SettingsPatch.PreferenceScreen.LAYOUT.addPreferences(
|
|
|
|
PreferenceScreen(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_preference_screen",
|
|
|
|
StringResource("revanced_alt_thumbnail_preference_screen_title", "Alternative thumbnails"),
|
2023-08-27 21:40:49 +02:00
|
|
|
listOf(
|
2023-12-11 02:07:09 +01:00
|
|
|
NonInteractivePreference(
|
|
|
|
StringResource("revanced_alt_thumbnail_about_title", "Thumbnails in use"),
|
|
|
|
null, // Summary is dynamically updated based on the current settings.
|
|
|
|
tag = "app.revanced.integrations.settingsmenu.AlternativeThumbnailsStatusPreference"
|
|
|
|
),
|
|
|
|
SwitchPreference(
|
|
|
|
"revanced_alt_thumbnail_dearrow",
|
2023-12-23 08:48:06 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_title", "Enable DeArrow thumbnails"),
|
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_summary_on", "Using DeArrow thumbnails"),
|
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_summary_off", "Not using DeArrow thumbnails")
|
2023-12-11 02:07:09 +01:00
|
|
|
),
|
|
|
|
SwitchPreference(
|
|
|
|
"revanced_alt_thumbnail_dearrow_connection_toast",
|
2023-12-12 17:51:26 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_connection_toast_title", "Show a toast if API is not available"),
|
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_connection_toast_summary_on", "Toast is shown if DeArrow is not available"),
|
|
|
|
StringResource("revanced_alt_thumbnail_dearrow_connection_toast_summary_off", "Toast is not shown if DeArrow is not available")
|
2023-12-11 02:07:09 +01:00
|
|
|
),
|
|
|
|
TextPreference(
|
|
|
|
"revanced_alt_thumbnail_dearrow_api_url",
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_dearrow_api_url_title",
|
|
|
|
"DeArrow API endpoint"
|
|
|
|
),
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_dearrow_api_url_summary",
|
|
|
|
"The URL of the DeArrow thumbnail cache endpoint. " +
|
|
|
|
"Do not change this unless you know what you\\\'re doing"
|
|
|
|
),
|
|
|
|
),
|
|
|
|
NonInteractivePreference(
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_dearrow_about_title",
|
|
|
|
"About DeArrow"
|
|
|
|
),
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_dearrow_about_summary",
|
2023-12-12 17:51:26 +01:00
|
|
|
"DeArrow provides crowd-sourced thumbnails for YouTube videos. " +
|
2023-12-11 02:07:09 +01:00
|
|
|
"These thumbnails are often more relevant than those provided by YouTube. " +
|
|
|
|
"If enabled, video URLs will be sent to the API server and no other data is sent."
|
|
|
|
+ "\\n\\nTap here to learn more about DeArrow"
|
|
|
|
),
|
|
|
|
// Custom about preference with link to the DeArrow website.
|
|
|
|
tag = "app.revanced.integrations.settingsmenu.AlternativeThumbnailsAboutDeArrowPreference",
|
|
|
|
selectable = true
|
|
|
|
),
|
2023-08-27 21:40:49 +02:00
|
|
|
SwitchPreference(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills",
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_title", "Enable still video captures"),
|
2023-12-12 17:51:26 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_stills_summary_on", "Using YouTube still video captures"),
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_summary_off", "Not using YouTube still video captures")
|
2023-08-27 21:40:49 +02:00
|
|
|
),
|
|
|
|
ListPreference(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills_time",
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_title", "Video time to take the still from"),
|
2023-08-27 21:40:49 +02:00
|
|
|
ArrayResource(
|
|
|
|
"revanced_alt_thumbnail_type_entries",
|
|
|
|
listOf(
|
2023-12-11 02:07:09 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_1", "Beginning of video"),
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_2", "Middle of video"),
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_3", "End of video"),
|
2023-08-27 21:40:49 +02:00
|
|
|
)
|
|
|
|
),
|
|
|
|
ArrayResource(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills_time_entry_values",
|
2023-08-27 21:40:49 +02:00
|
|
|
listOf(
|
2023-12-11 02:07:09 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_value_1", "1"),
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_value_2", "2"),
|
|
|
|
StringResource("revanced_alt_thumbnail_stills_time_entry_value_3", "3"),
|
2023-08-27 21:40:49 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
SwitchPreference(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills_fast",
|
2023-09-20 05:33:02 +02:00
|
|
|
StringResource(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills_fast_title",
|
|
|
|
"Use fast still captures"
|
2023-09-20 05:33:02 +02:00
|
|
|
),
|
2023-12-11 02:07:09 +01:00
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_stills_fast_summary_on",
|
|
|
|
"Using medium quality still captures. " +
|
|
|
|
"Thumbnails will load faster, but live streams, unreleased, " +
|
|
|
|
"or very old videos may show blank thumbnails"
|
|
|
|
),
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_stills_fast_summary_off",
|
|
|
|
"Using high quality still captures"
|
|
|
|
)
|
2023-08-27 21:40:49 +02:00
|
|
|
),
|
|
|
|
NonInteractivePreference(
|
2023-09-20 05:33:02 +02:00
|
|
|
StringResource(
|
2023-12-11 02:07:09 +01:00
|
|
|
"revanced_alt_thumbnail_stills_about_title",
|
|
|
|
"About still video captures"
|
|
|
|
),
|
|
|
|
StringResource(
|
|
|
|
"revanced_alt_thumbnail_stills_about_summary",
|
|
|
|
"Still captures are taken from the beginning/middle/end of each video. " +
|
|
|
|
"These images are built into YouTube and no external API is used"
|
|
|
|
),
|
|
|
|
// Restore the preference dividers to keep it from looking weird.
|
|
|
|
selectable = true
|
2023-08-27 21:40:49 +02:00
|
|
|
)
|
|
|
|
),
|
2023-12-11 02:07:09 +01:00
|
|
|
StringResource("revanced_alt_thumbnail_preference_screen_summary", "Video thumbnail settings")
|
2023-08-27 21:40:49 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-12-11 02:07:09 +01:00
|
|
|
fun MethodFingerprint.getResultOrThrow() =
|
|
|
|
result ?: throw exception
|
2023-08-27 21:40:49 +02:00
|
|
|
|
2023-12-11 02:07:09 +01:00
|
|
|
fun MethodFingerprint.alsoResolve(fingerprint: MethodFingerprint) =
|
|
|
|
also { resolve(context, fingerprint.getResultOrThrow().classDef) }.getResultOrThrow()
|
2023-08-27 21:40:49 +02:00
|
|
|
|
2023-12-11 02:07:09 +01:00
|
|
|
fun MethodFingerprint.resolveAndLetMutableMethod(
|
|
|
|
fingerprint: MethodFingerprint,
|
|
|
|
block: (MutableMethod) -> Unit
|
|
|
|
) = alsoResolve(fingerprint).also { block(it.mutableMethod) }
|
2023-08-27 21:40:49 +02:00
|
|
|
|
2023-12-11 02:07:09 +01:00
|
|
|
MessageDigestImageUrlFingerprint.resolveAndLetMutableMethod(MessageDigestImageUrlParentFingerprint) {
|
|
|
|
loadImageUrlMethod = it
|
|
|
|
addImageUrlHook(INTEGRATIONS_CLASS_DESCRIPTOR, true)
|
|
|
|
}
|
2023-08-27 21:40:49 +02:00
|
|
|
|
2023-12-11 02:07:09 +01:00
|
|
|
OnSucceededFingerprint.resolveAndLetMutableMethod(OnResponseStartedFingerprint) {
|
|
|
|
loadImageSuccessCallbackMethod = it
|
|
|
|
addImageUrlSuccessCallbackHook(INTEGRATIONS_CLASS_DESCRIPTOR)
|
|
|
|
}
|
|
|
|
|
|
|
|
OnFailureFingerprint.resolveAndLetMutableMethod(OnResponseStartedFingerprint) {
|
|
|
|
loadImageErrorCallbackMethod = it
|
|
|
|
addImageUrlErrorCallbackHook(INTEGRATIONS_CLASS_DESCRIPTOR)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The URL is required for the failure callback hook, but the URL field is obfuscated.
|
|
|
|
// Add a helper get method that returns the URL field.
|
|
|
|
RequestFingerprint.getResultOrThrow().apply {
|
|
|
|
// The url is the only string field that is set inside the constructor.
|
|
|
|
val urlFieldInstruction = mutableMethod.getInstructions().first {
|
|
|
|
if (it.opcode != Opcode.IPUT_OBJECT) return@first false
|
|
|
|
|
|
|
|
val reference = (it as ReferenceInstruction).reference as FieldReference
|
|
|
|
reference.type == "Ljava/lang/String;"
|
|
|
|
} as ReferenceInstruction
|
|
|
|
|
|
|
|
val urlFieldName = (urlFieldInstruction.reference as FieldReference).name
|
|
|
|
val definingClass = RequestFingerprint.IMPLEMENTATION_CLASS_NAME
|
|
|
|
val addedMethodName = "getHookedUrl"
|
|
|
|
mutableClass.methods.add(
|
|
|
|
ImmutableMethod(
|
|
|
|
definingClass,
|
|
|
|
addedMethodName,
|
|
|
|
emptyList(),
|
|
|
|
"Ljava/lang/String;",
|
|
|
|
AccessFlags.PUBLIC.value,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
MutableMethodImplementation(2)
|
|
|
|
).toMutable().apply {
|
|
|
|
addInstructions(
|
|
|
|
"""
|
|
|
|
iget-object v0, p0, $definingClass->${urlFieldName}:Ljava/lang/String;
|
|
|
|
return-object v0
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2023-08-27 21:40:49 +02:00
|
|
|
}
|
|
|
|
}
|