Compare commits

...
Sign in to create a new pull request.

6 commits

35 changed files with 525 additions and 198 deletions

View file

@ -4,10 +4,22 @@ import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class RememberClearDisplayPatch { public class RememberClearDisplayPatch {
private static Boolean cachedState = null;
public static boolean getClearDisplayState() { public static boolean getClearDisplayState() {
return Settings.CLEAR_DISPLAY.get(); if (cachedState == null) {
cachedState = Settings.CLEAR_DISPLAY.get();
}
return cachedState;
} }
public static void rememberClearDisplayState(boolean newState) { public static void rememberClearDisplayState(boolean newState) {
if (cachedState != null && cachedState == newState) {
return;
}
cachedState = newState;
Settings.CLEAR_DISPLAY.save(newState); Settings.CLEAR_DISPLAY.save(newState);
} }
} }

View file

@ -1,14 +1,43 @@
package app.revanced.extension.tiktok.download; package app.revanced.extension.tiktok.download;
import app.revanced.extension.tiktok.settings.Settings; import com.ss.android.ugc.aweme.feed.model.Video;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class DownloadsPatch { public class DownloadsPatch {
public static String getDownloadPath() { public static String getDownloadPath() {
return Settings.DOWNLOAD_PATH.get(); return "Pictures/Tiktok";
//return Settings.DOWNLOAD_PATH.get();
} }
public static boolean shouldRemoveWatermark() { public static boolean shouldRemoveWatermark() {
return Settings.DOWNLOAD_WATERMARK.get(); return true;
//return Settings.DOWNLOAD_WATERMARK.get();
} }
}
public static void patchVideoObject(Video video) {
if (video == null) return;
try {
boolean isMissingCleanUrl = false;
// non-watermark url is removed by tiktok for some videos (licensing/user restrictions)
if (video.downloadNoWatermarkAddr == null) {
isMissingCleanUrl = true;
} else if (video.downloadNoWatermarkAddr.getUrlList() == null || video.downloadNoWatermarkAddr.getUrlList().isEmpty()) {
isMissingCleanUrl = true;
}
// overwrite field with the play address if empty
if (isMissingCleanUrl) {
if (video.h264PlayAddr != null && video.h264PlayAddr.getUrlList() != null && !video.h264PlayAddr.getUrlList().isEmpty()) {
video.downloadNoWatermarkAddr = video.h264PlayAddr;
} else if (video.playAddr != null) {
// fallback
video.downloadNoWatermarkAddr = video.playAddr;
}
}
} catch (Throwable t) {
}
}
}

View file

@ -6,11 +6,35 @@ import com.ss.android.ugc.aweme.feed.model.Aweme;
public class AdsFilter implements IFilter { public class AdsFilter implements IFilter {
@Override @Override
public boolean getEnabled() { public boolean getEnabled() {
return Settings.REMOVE_ADS.get(); return true;
// return Settings.REMOVE_ADS.get();
} }
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
return item.isAd() || item.isWithPromotionalMusic(); if (item == null) return false;
// TikTok's Internal Commercial Types
// Verified in AwemeExtKt: 1, 29, 30, 32, 33, 201 are commercial
int type = item.getAwemeType();
if (type == 1 || type == 29 || type == 30 || type == 32 || type == 33 || type == 201) {
return true;
}
// Ad Flags (Hard and Soft/Sponsored)
if (item.isAd || item.isSoftAd || item.awemeRawAd != null) {
return true;
}
// Music Marketing
if (item.isWithPromotionalMusic()) return true;
if (item.mCommerceVideoAuthInfo != null) {
// PseudoAds (Spark Ads) and Branded Content
return item.mCommerceVideoAuthInfo.isBrandedContent() ||
item.mCommerceVideoAuthInfo.isPseudoAd();
}
return false;
} }
} }

View file

@ -0,0 +1,28 @@
package app.revanced.extension.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class BloatFilter implements IFilter {
@Override
public boolean getEnabled() {
return true;
}
@Override
public boolean getFiltered(Aweme item) {
if (item == null) return false;
// Full screen promos
if (item.isReferralFakeAweme || item.isRecBigCardFakeAweme) {
return true;
}
// System cards (non video interrupts)
if (item.awemeType == 104 || item.awemeType == 105) return true;
// Accounts to follow recs and overlays
if (item.recommendCardType != 0) return true;
return false;
}
}

View file

@ -8,31 +8,41 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
public final class FeedItemsFilter { public final class FeedItemsFilter {
private static final List<IFilter> FILTERS = List.of(
new AdsFilter(), private static final IFilter[] FILTERS = new IFilter[] {
new LiveFilter(), new AdsFilter(),
new StoryFilter(), new LiveFilter(),
new ImageVideoFilter(), new ShopFilter(),
new ViewCountFilter(), new StoryFilter(),
new LikeCountFilter(), new ImageVideoFilter(),
new ShopFilter() new BloatFilter()
); };
public static void filter(FeedItemList feedItemList) { public static void filter(FeedItemList feedItemList) {
if (feedItemList == null || feedItemList.items == null) return;
filterFeedList(feedItemList.items, item -> item); filterFeedList(feedItemList.items, item -> item);
} }
public static void filter(FollowFeedList followFeedList) { public static void filter(FollowFeedList followFeedList) {
if (followFeedList == null || followFeedList.mItems == null) return;
filterFeedList(followFeedList.mItems, feed -> (feed != null) ? feed.aweme : null); filterFeedList(followFeedList.mItems, feed -> (feed != null) ? feed.aweme : null);
} }
private static <T> void filterFeedList(List<T> list, AwemeExtractor<T> extractor) { private static <T> void filterFeedList(
// Could be simplified with removeIf() but requires Android 7.0+ while TikTok supports 4.0+. List<T> list,
AwemeExtractor<T> extractor
) {
if (list == null) return;
Iterator<T> iterator = list.iterator(); Iterator<T> iterator = list.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
T container = iterator.next(); T container = iterator.next();
Aweme item = extractor.extract(container); Aweme item = extractor.extract(container);
if (item != null && shouldFilter(item)) {
if (item == null) continue;
if (shouldFilter(item)) {
iterator.remove(); iterator.remove();
} }
} }

View file

@ -11,6 +11,17 @@ public class ImageVideoFilter implements IFilter {
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
return item.isImage() || item.isPhotoMode(); if (item == null) return false;
int type = item.getAwemeType();
// 2 = Standard Image, 150 = Photo Mode, 160 = Text Mode
if (type == 2 || type == 150 || type == 160) {
return true;
}
// Fallback checks
var imageInfos = item.getImageInfos();
return imageInfos != null && !imageInfos.isEmpty();
} }
} }

View file

@ -24,7 +24,10 @@ public final class LikeCountFilter implements IFilter {
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics(); AwemeStatistics statistics = item.statistics;
if (statistics == null) statistics = item.getStatistics();
if (statistics == null) return false; if (statistics == null) return false;
long likeCount = statistics.getDiggCount(); long likeCount = statistics.getDiggCount();

View file

@ -6,11 +6,19 @@ import com.ss.android.ugc.aweme.feed.model.Aweme;
public class LiveFilter implements IFilter { public class LiveFilter implements IFilter {
@Override @Override
public boolean getEnabled() { public boolean getEnabled() {
return Settings.HIDE_LIVE.get(); return true;
} }
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
return item.isLive() || item.isLiveReplay(); if (item == null) return false;
// awemeType 101 is the 'isLive' check in code
if (item.getAwemeType() == 101 || item.getRoom() != null) {
return true;
}
// Fallbacks
return item.isLiveReplay() || item.getLiveId() != 0 || item.getLiveType() != null;
} }
} }

View file

@ -4,14 +4,33 @@ import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme; import com.ss.android.ugc.aweme.feed.model.Aweme;
public class ShopFilter implements IFilter { public class ShopFilter implements IFilter {
private static final String SHOP_INFO = "placeholder_product_id";
@Override @Override
public boolean getEnabled() { public boolean getEnabled() {
return Settings.HIDE_SHOP.get(); return true;
// return Settings.HIDE_SHOP.get();
} }
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
return item.getShareUrl().contains(SHOP_INFO); if (item == null) return false;
// Attached Products (TikTok Shop)
if (item.productsInfo != null && !item.productsInfo.isEmpty()) {
return true;
}
// Simple Promotions (Banner links)
if (item.simplePromotions != null && !item.simplePromotions.isEmpty()) {
return true;
}
// Shop Ads
if (item.shopAdStruct != null) {
return true;
}
// Fallback (URL check)
String shareUrl = item.getShareUrl();
return shareUrl != null && shareUrl.contains("placeholder_product_id");
} }
} }

View file

@ -9,8 +9,17 @@ public class StoryFilter implements IFilter {
return Settings.HIDE_STORY.get(); return Settings.HIDE_STORY.get();
} }
@Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
return item.getIsTikTokStory(); if (item == null) return false;
if (item.isTikTokStory) return true;
// Type 40 = Standard Story, 11 = Legacy/Region Story
int type = item.getAwemeType();
if (type == 40 || type == 11 || item.isTikTokStory) {
return true;
}
return false;
} }
} }

View file

@ -23,7 +23,11 @@ public class ViewCountFilter implements IFilter {
@Override @Override
public boolean getFiltered(Aweme item) { public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics(); AwemeStatistics statistics = item.statistics;
// Fallback to getter if field is null
if (statistics == null) statistics = item.getStatistics();
if (statistics == null) return false; if (statistics == null) return false;
long playCount = statistics.getPlayCount(); long playCount = statistics.getPlayCount();

View file

@ -13,7 +13,8 @@ public final class ShareUrlSanitizer {
* Injection point for setting check. * Injection point for setting check.
*/ */
public static boolean shouldSanitize() { public static boolean shouldSanitize() {
return BaseSettings.SANITIZE_SHARED_LINKS.get(); return true;
// return BaseSettings.SANITIZE_SHARED_LINKS.get();
} }
/** /**

View file

@ -0,0 +1,7 @@
package com.ss.android.ugc.aweme.base.model;
import java.util.List;
public class UrlModel {
public List<String> getUrlList() { throw new UnsupportedOperationException("Stub"); }
public String getUri() { throw new UnsupportedOperationException("Stub"); }
}

View file

@ -0,0 +1,17 @@
package com.ss.android.ugc.aweme.commerce;
import com.ss.android.ugc.aweme.feed.model.AwemeRawAd;
public class AwemeCommerceStruct {
public boolean isBrandedContent() {
throw new UnsupportedOperationException("Stub");
}
public boolean isPseudoAd() {
throw new UnsupportedOperationException("Stub");
}
public AwemeRawAd getPseudoAdData() {
throw new UnsupportedOperationException("Stub");
}
}

View file

@ -0,0 +1,4 @@
package com.ss.android.ugc.aweme.commerce.model;
public class ShopAdStruct {
}

View file

@ -0,0 +1,4 @@
package com.ss.android.ugc.aweme.commerce.model;
public class SimplePromotion {
}

View file

@ -1,40 +1,57 @@
package com.ss.android.ugc.aweme.feed.model; package com.ss.android.ugc.aweme.feed.model;
//Dummy class import com.ss.android.ugc.aweme.commerce.model.ShopAdStruct;
import com.ss.android.ugc.aweme.commerce.model.SimplePromotion;
import com.ss.android.ugc.aweme.search.ecom.data.Product;
import com.ss.android.ugc.aweme.commerce.AwemeCommerceStruct;
import java.util.List;
public class Aweme { public class Aweme {
public boolean isAd() {
throw new UnsupportedOperationException("Stub"); // Internal Feed Type Identifiers
} public int awemeType;
public int adLinkType;
// Live Stream Data
public RoomStruct room;
// Monetization & Sponsored Traffic
public boolean isAd;
public boolean isSoftAd;
public AwemeRawAd awemeRawAd;
public AwemeCommerceStruct mCommerceVideoAuthInfo;
public boolean isLive() { // E-Commerce / Shop Data
throw new UnsupportedOperationException("Stub"); public List<Object> productsInfo;
} public List<Object> simplePromotions;
public ShopAdStruct shopAdStruct;
// Non-Video Feed Injections (Fake Awemes)
public boolean isReferralFakeAweme;
public boolean isRecBigCardFakeAweme;
public boolean isLiveReplay() { // Social & Follow Recommendations
throw new UnsupportedOperationException("Stub"); public int recommendCardType;
} public List<Object> familiarRecommendUser;
public boolean isWithPromotionalMusic() { // Content Engagement Statistics
throw new UnsupportedOperationException("Stub"); public AwemeStatistics statistics;
}
public boolean getIsTikTokStory() { // Story Metadata
throw new UnsupportedOperationException("Stub"); public boolean isTikTokStory;
}
public boolean isImage() { public int getAwemeType() { throw new UnsupportedOperationException("Stub"); }
throw new UnsupportedOperationException("Stub"); public RoomStruct getRoom() { throw new UnsupportedOperationException("Stub"); }
} public boolean isAd() { throw new UnsupportedOperationException("Stub"); }
public boolean isSoftAd() { throw new UnsupportedOperationException("Stub"); }
public boolean isPhotoMode() { public AwemeStatistics getStatistics() { throw new UnsupportedOperationException("Stub"); }
throw new UnsupportedOperationException("Stub");
} // Stub methods for legacy compatibility
public String getAid() { throw new UnsupportedOperationException("Stub"); }
public AwemeStatistics getStatistics() { public boolean isLiveReplay() { throw new UnsupportedOperationException("Stub"); }
throw new UnsupportedOperationException("Stub"); public long getLiveId() { throw new UnsupportedOperationException("Stub"); }
} public String getLiveType() { throw new UnsupportedOperationException("Stub"); }
public boolean isWithPromotionalMusic() { throw new UnsupportedOperationException("Stub"); }
public String getShareUrl() { public String getShareUrl() { throw new UnsupportedOperationException("Stub"); }
throw new UnsupportedOperationException("Stub"); public List getImageInfos() { throw new UnsupportedOperationException("Stub"); }
} }
}

View file

@ -0,0 +1,4 @@
package com.ss.android.ugc.aweme.feed.model;
public class AwemeRawAd {
}

View file

@ -1,10 +1,13 @@
package com.ss.android.ugc.aweme.feed.model; package com.ss.android.ugc.aweme.feed.model;
public class AwemeStatistics { public class AwemeStatistics {
// Used by ViewCountFilter
public long getPlayCount() { public long getPlayCount() {
throw new UnsupportedOperationException("Stub"); throw new UnsupportedOperationException("Stub");
} }
public long getDiggCount() { public long getDiggCount() {
throw new UnsupportedOperationException("Stub"); throw new UnsupportedOperationException("Stub");
} }
} }

View file

@ -0,0 +1,6 @@
package com.ss.android.ugc.aweme.feed.model;
// Dummy class
public class PhotoModeImageInfo {
}

View file

@ -0,0 +1,6 @@
package com.ss.android.ugc.aweme.feed.model;
// Dummy class
public class PhotoModeTextInfo {
}

View file

@ -0,0 +1,4 @@
package com.ss.android.ugc.aweme.feed.model;
public class RoomStruct {
}

View file

@ -0,0 +1,8 @@
package com.ss.android.ugc.aweme.feed.model;
import com.ss.android.ugc.aweme.base.model.UrlModel;
public class Video {
public VideoUrlModel playAddr;
public VideoUrlModel h264PlayAddr;
public UrlModel downloadNoWatermarkAddr;
}

View file

@ -0,0 +1,5 @@
package com.ss.android.ugc.aweme.feed.model;
import com.ss.android.ugc.aweme.base.model.UrlModel;
public class VideoUrlModel extends UrlModel {
}

View file

@ -0,0 +1,4 @@
package com.ss.android.ugc.aweme.search.ecom.data;
public class Product {
}

View file

@ -1,11 +1,7 @@
package app.revanced.patches.tiktok.feedfilter package app.revanced.patches.tiktok.feedfilter
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch
import app.revanced.patches.tiktok.misc.settings.settingsPatch
import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@ -17,33 +13,43 @@ val feedFilterPatch = bytecodePatch(
description = "Removes ads, livestreams, stories, image videos " + description = "Removes ads, livestreams, stories, image videos " +
"and videos with a specific amount of views or likes from the feed.", "and videos with a specific amount of views or likes from the feed.",
) { ) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
)
compatibleWith( compatibleWith(
"com.ss.android.ugc.trill"("36.5.4"), "com.ss.android.ugc.trill"("43.8.3"),
"com.zhiliaoapp.musically"("36.5.4"), "com.zhiliaoapp.musically"("43.8.3"),
) )
execute { execute {
arrayOf( feedItemListGetItemsFingerprint.method.let { method ->
feedApiServiceLIZFingerprint.method to "$EXTENSION_CLASS_DESCRIPTOR->filter(Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;)V", val returnIndices = method.implementation!!.instructions.withIndex()
followFeedFingerprint.method to "$EXTENSION_CLASS_DESCRIPTOR->filter(Lcom/ss/android/ugc/aweme/follow/presenter/FollowFeedList;)V" .filter { it.value.opcode == Opcode.RETURN_OBJECT }
).forEach { (method, filterSignature) -> .map { it.index }
val returnInstruction = method.instructions.first { it.opcode == Opcode.RETURN_OBJECT } .toList()
val register = (returnInstruction as OneRegisterInstruction).registerA
method.addInstruction( returnIndices.asReversed().forEach { returnIndex ->
returnInstruction.location.index, method.addInstructions(
"invoke-static { v$register }, $filterSignature" returnIndex,
) "invoke-static {p0}, $EXTENSION_CLASS_DESCRIPTOR->filter(Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;)V"
)
}
} }
settingsStatusLoadFingerprint.method.addInstruction( followFeedFingerprint.method.let { method ->
0, val returnIndices = method.implementation!!.instructions.withIndex()
"invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableFeedFilter()V", .filter { it.value.opcode == Opcode.RETURN_OBJECT }
) .map { it.index }
} .toList()
} returnIndices.asReversed().forEach { returnIndex ->
val register = (method.implementation!!.instructions[returnIndex] as OneRegisterInstruction).registerA
method.addInstructions(
returnIndex,
"""
if-nez v$register, :skip
invoke-static/range { v$register .. v$register }, $EXTENSION_CLASS_DESCRIPTOR->filter(Lcom/ss/android/ugc/aweme/follow/presenter/FollowFeedList;)V
:skip
"""
)
}
}
}
}

View file

@ -2,21 +2,22 @@ package app.revanced.patches.tiktok.feedfilter
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val feedApiServiceLIZFingerprint = fingerprint {
internal val feedItemListGetItemsFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Ljava/util/List;")
custom { method, classDef -> custom { method, classDef ->
classDef.endsWith("/FeedApiService;") && method.name == "fetchFeedList" classDef.endsWith("/FeedItemList;") &&
method.name == "getItems" &&
method.parameterTypes.isEmpty()
} }
} }
internal val followFeedFingerprint = fingerprint { internal val followFeedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("Lcom/ss/android/ugc/aweme/follow/presenter/FollowFeedList;") returns("Lcom/ss/android/ugc/aweme/follow/presenter/FollowFeedList;")
strings("getFollowFeedList") custom { method, _ ->
opcodes( method.parameterTypes.size == 2
Opcode.INVOKE_INTERFACE_RANGE, }
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_INTERFACE
)
} }

View file

@ -1,6 +1,7 @@
package app.revanced.patches.tiktok.interaction.cleardisplay package app.revanced.patches.tiktok.interaction.cleardisplay
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal val onClearDisplayEventFingerprint = fingerprint { internal val onClearDisplayEventFingerprint = fingerprint {
custom { method, classDef -> custom { method, classDef ->
@ -8,3 +9,47 @@ internal val onClearDisplayEventFingerprint = fingerprint {
classDef.endsWith("/ClearModePanelComponent;") && method.name == "onClearModeEvent" classDef.endsWith("/ClearModePanelComponent;") && method.name == "onClearModeEvent"
} }
} }
internal val clearModeLogCoreFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
returns("V")
parameters(
"Z",
"Ljava/lang/String;",
"Ljava/lang/String;",
"Lcom/ss/android/ugc/aweme/feed/model/Aweme;",
"Ljava/lang/String;",
"J",
"I"
)
}
internal val clearModeLogStateFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
returns("V")
parameters(
"Lcom/bytedance/common/utility/collection/WeakHandler;",
"Z",
"Ljava/lang/String;",
"Lcom/ss/android/ugc/aweme/feed/model/Aweme;",
"J"
"I",
"I"
)
}
internal val clearModeLogPlaytimeFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("V")
parameters(
"F",
"I",
"J",
"J",
"Lcom/ss/android/ugc/aweme/feed/model/Aweme;",
"Ljava/lang/String;",
"Ljava/lang/String;",
"Z",
"Z"
)
}

View file

@ -1,12 +1,11 @@
package app.revanced.patches.tiktok.interaction.cleardisplay package app.revanced.patches.tiktok.interaction.cleardisplay
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.tiktok.shared.onRenderFirstFrameFingerprint import app.revanced.patches.tiktok.shared.onRenderFirstFrameFingerprint
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
@ -16,14 +15,19 @@ val rememberClearDisplayPatch = bytecodePatch(
description = "Remembers the clear display configurations in between videos.", description = "Remembers the clear display configurations in between videos.",
) { ) {
compatibleWith( compatibleWith(
"com.ss.android.ugc.trill"("36.5.4"), "com.ss.android.ugc.trill"("43.8.3"),
"com.zhiliaoapp.musically"("36.5.4"), "com.zhiliaoapp.musically"("43.8.3"),
) )
execute { execute {
onClearDisplayEventFingerprint.method.let { // kill loggers to prevent db from being constantly logged to
// region Hook the "Clear display" configuration save event to remember the state of clear display. // might resolve crashing issue with this patch
clearModeLogCoreFingerprint.method.returnEarly()
clearModeLogStateFingerprint.method.returnEarly()
clearModeLogPlaytimeFingerprint.method.returnEarly()
onClearDisplayEventFingerprint.method.let {
// Hook the "Clear display" configuration save event to remember the state of clear display.
val isEnabledIndex = it.indexOfFirstInstructionOrThrow(Opcode.IGET_BOOLEAN) + 1 val isEnabledIndex = it.indexOfFirstInstructionOrThrow(Opcode.IGET_BOOLEAN) + 1
val isEnabledRegister = it.getInstruction<TwoRegisterInstruction>(isEnabledIndex - 1).registerA val isEnabledRegister = it.getInstruction<TwoRegisterInstruction>(isEnabledIndex - 1).registerA
@ -33,38 +37,40 @@ val rememberClearDisplayPatch = bytecodePatch(
"Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->rememberClearDisplayState(Z)V", "Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->rememberClearDisplayState(Z)V",
) )
// endregion
// region Override the "Clear display" configuration load event to load the state of clear display.
val clearDisplayEventClass = it.parameters[0].type val clearDisplayEventClass = it.parameters[0].type
onRenderFirstFrameFingerprint.method.addInstructionsWithLabels( onRenderFirstFrameFingerprint.method.addInstructions(
0, 0,
""" """
# Create a new clearDisplayEvent and post it to the EventBus (https://github.com/greenrobot/EventBus) # Get the saved state
# Clear display type such as 0 = LONG_PRESS, 1 = SCREEN_RECORD etc.
const/4 v1, 0x0
# Enter method (Such as "pinch", "swipe_exit", or an empty string (unknown, what it means)).
const-string v2, ""
# Name of the clear display type which is equivalent to the clear display type.
const-string v3, "long_press"
# The state of clear display.
invoke-static { }, Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->getClearDisplayState()Z invoke-static { }, Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->getClearDisplayState()Z
move-result v4 move-result v1
if-eqz v4, :clear_display_disabled
# If false, jump past the event post
if-eqz v1, :clear_display_disabled
# Set up the other parameters
# Clear display type: 0 = LONG_PRESS
const/4 v2, 0x0
# Enter method
const-string v3, ""
# Name of the clear display type
const-string v4, "long_press"
# Create the event
new-instance v0, $clearDisplayEventClass new-instance v0, $clearDisplayEventClass
invoke-direct { v0, v1, v2, v3, v4 }, $clearDisplayEventClass-><init>(ILjava/lang/String;Ljava/lang/String;Z)V
# Call the constructor in order
invoke-direct { v0, v1, v2, v3, v4 }, $clearDisplayEventClass-><init>(ZILjava/lang/String;Ljava/lang/String;)V
# Post it to the EventBus
invoke-virtual { v0 }, $clearDisplayEventClass->post()Lcom/ss/android/ugc/governance/eventbus/IEvent; invoke-virtual { v0 }, $clearDisplayEventClass->post()Lcom/ss/android/ugc/governance/eventbus/IEvent;
""",
ExternalLabel("clear_display_disabled", onRenderFirstFrameFingerprint.method.getInstruction(0)),
)
// endregion :clear_display_disabled
nop
"""
)
} }
} }
} }

View file

@ -7,13 +7,14 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch
import app.revanced.patches.tiktok.misc.settings.settingsPatch
import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint
import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.returnEarly import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/tiktok/download/DownloadsPatch;" private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/tiktok/download/DownloadsPatch;"
@ -25,19 +26,17 @@ val downloadsPatch = bytecodePatch(
) { ) {
dependsOn( dependsOn(
sharedExtensionPatch, sharedExtensionPatch,
settingsPatch,
) )
compatibleWith( compatibleWith(
"com.ss.android.ugc.trill"("36.5.4"), "com.ss.android.ugc.trill"("43.8.3"),
"com.zhiliaoapp.musically"("36.5.4"), "com.zhiliaoapp.musically"("43.8.3"),
) )
execute { execute {
aclCommonShareFingerprint.method.returnEarly(0) aclCommonShareFingerprint.method.returnEarly(0)
aclCommonShare2Fingerprint.method.returnEarly(2) aclCommonShare2Fingerprint.method.returnEarly(2)
// Download videos without watermark.
aclCommonShare3Fingerprint.method.addInstructionsWithLabels( aclCommonShare3Fingerprint.method.addInstructionsWithLabels(
0, 0,
""" """
@ -51,7 +50,51 @@ val downloadsPatch = bytecodePatch(
""", """,
) )
// Change the download path patch. awemeGetVideoFingerprint.method.apply {
val returnIndex = findInstructionIndicesReversedOrThrow { opcode == Opcode.RETURN_OBJECT }.first()
val register = getInstruction<OneRegisterInstruction>(returnIndex).registerA
addInstructions(
returnIndex,
"""
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->patchVideoObject(Lcom/ss/android/ugc/aweme/feed/model/Video;)V
"""
)
}
commentImageWatermarkFingerprint.method.apply {
val drawBitmapIndex = findInstructionIndicesReversedOrThrow {
opcode.name == "invoke-virtual" &&
this is ReferenceInstruction &&
reference.toString().contains("->drawBitmap(Landroid/graphics/Bitmap;FFLandroid/graphics/Paint;)V")
}.first()
val drawInstr = getInstruction<FiveRegisterInstruction>(drawBitmapIndex)
val canvasReg = drawInstr.registerC
val bitmapReg = drawInstr.registerD
val xReg = drawInstr.registerE
val yReg = drawInstr.registerF
val paintReg = drawInstr.registerG
removeInstructions(drawBitmapIndex, 1)
addInstructionsWithLabels(
drawBitmapIndex,
"""
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldRemoveWatermark()Z
move-result v$xReg
if-nez v$xReg, :skip_watermark
const/4 v$xReg, 0x0
invoke-virtual {v$canvasReg, v$bitmapReg, v$xReg, v$yReg, v$paintReg}, Landroid/graphics/Canvas;->drawBitmap(Landroid/graphics/Bitmap;FFLandroid/graphics/Paint;)V
:skip_watermark
nop
"""
)
}
downloadUriFingerprint.method.apply { downloadUriFingerprint.method.apply {
findInstructionIndicesReversedOrThrow { findInstructionIndicesReversedOrThrow {
getReference<FieldReference>().let { getReference<FieldReference>().let {
@ -80,4 +123,4 @@ val downloadsPatch = bytecodePatch(
"invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableDownload()V", "invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableDownload()V",
) )
} }
} }

View file

@ -44,3 +44,19 @@ internal val downloadUriFingerprint = fingerprint {
"video/mp4" "video/mp4"
) )
} }
internal val awemeGetVideoFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Lcom/ss/android/ugc/aweme/feed/model/Video;")
custom { method, classDef ->
classDef.endsWith("/Aweme;") &&
method.name == "getVideo" &&
method.parameterTypes.isEmpty()
}
}
internal val commentImageWatermarkFingerprint = fingerprint {
strings("[tiktok_logo]", "image/jpeg", "is_pending")
parameters("Landroid/graphics/Bitmap;")
returns("V")
}

View file

@ -10,8 +10,8 @@ internal val getSpeedFingerprint = fingerprint {
} }
internal val setSpeedFingerprint = fingerprint { internal val setSpeedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V") returns("Ljava/lang/Object;")
parameters("Ljava/lang/String;", "Lcom/ss/android/ugc/aweme/feed/model/Aweme;", "F") strings("playback_speed")
strings("enterFrom") custom { method, _ -> method.name == "invoke" && method.parameterTypes.isEmpty() }
} }

View file

@ -16,6 +16,7 @@ val playbackSpeedPatch = bytecodePatch(
name = "Playback speed", name = "Playback speed",
description = "Enables the playback speed option for all videos and " + description = "Enables the playback speed option for all videos and " +
"retains the speed configurations in between videos.", "retains the speed configurations in between videos.",
use = false
) { ) {
compatibleWith( compatibleWith(
"com.ss.android.ugc.trill"("36.5.4"), "com.ss.android.ugc.trill"("36.5.4"),

View file

@ -5,21 +5,22 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
internal val urlShorteningFingerprint = fingerprint { internal val urlShorteningFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("LX/") returns("V")
parameters( parameters(
"I", "L",
"Ljava/lang/String;", "Ljava/lang/String;",
"Ljava/util/List;",
"Ljava/lang/String;", "Ljava/lang/String;",
"Ljava/lang/String;" "Z",
"I"
) )
opcodes(Opcode.RETURN_OBJECT) opcodes(Opcode.RETURN_VOID)
// Same Kotlin intrinsics literal on both variants. // Same Kotlin intrinsics literal on both variants.
strings("getShortShareUrlObservab\u2026ongUrl, subBizSceneValue)") strings("share_link_id", "invitation_scene")
custom { method, _ -> custom { method, _ ->
// LIZLLL is obfuscated by ProGuard/R8, but stable across both TikTok and Musically. method.parameterTypes.size == 6
method.name == "LIZLLL"
} }
} }

View file

@ -26,58 +26,19 @@ val sanitizeShareUrlsPatch = bytecodePatch(
dependsOn(sharedExtensionPatch) dependsOn(sharedExtensionPatch)
compatibleWith( compatibleWith(
"com.ss.android.ugc.trill"("36.5.4"), "com.ss.android.ugc.trill"("43.8.3"),
"com.zhiliaoapp.musically"("36.5.4"), "com.zhiliaoapp.musically"("43.8.3"),
) )
execute { execute {
urlShorteningFingerprint.method.apply { urlShorteningFingerprint.method.apply {
val invokeIndex = indexOfFirstInstructionOrThrow { val longUrlRegister = implementation!!.registerCount - 6 + 3
val ref = getReference<MethodReference>()
ref?.name == "LIZ" && ref.definingClass.startsWith("LX/")
}
val moveResultIndex = indexOfFirstInstructionOrThrow(invokeIndex, Opcode.MOVE_RESULT_OBJECT) addInstructions(
val urlRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA 0,
// Resolve Observable wrapper classes at runtime
val observableWrapperIndex = indexOfFirstInstructionOrThrow(Opcode.NEW_INSTANCE)
val observableWrapperClass = getInstruction<ReferenceInstruction>(observableWrapperIndex)
.reference.toString()
val observableFactoryIndex = indexOfFirstInstructionOrThrow {
val ref = getReference<MethodReference>()
ref?.name == "LJ" && ref.definingClass.startsWith("LX/")
}
val observableFactoryRef = getInstruction<ReferenceInstruction>(observableFactoryIndex)
.reference as MethodReference
val observableFactoryClass = observableFactoryRef.definingClass
val observableInterfaceType = observableFactoryRef.parameterTypes.first()
val observableReturnType = observableFactoryRef.returnType
val wrapperRegister = findFreeRegister(moveResultIndex + 1, urlRegister)
// Check setting and conditionally sanitize share URL.
addInstructionsWithLabels(
moveResultIndex + 1,
""" """
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldSanitize()Z invoke-static/range { v$longUrlRegister .. v$longUrlRegister }, $EXTENSION_CLASS_DESCRIPTOR->sanitizeShareUrl(Ljava/lang/String;)Ljava/lang/String;
move-result v$wrapperRegister move-result-object v$longUrlRegister
if-eqz v$wrapperRegister, :skip_sanitization
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->sanitizeShareUrl(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$urlRegister
# Wrap sanitized URL and return early to bypass ShareExtService
new-instance v$wrapperRegister, $observableWrapperClass
invoke-direct { v$wrapperRegister, v$urlRegister }, $observableWrapperClass-><init>(Ljava/lang/String;)V
invoke-static { v$wrapperRegister }, $observableFactoryClass->LJ($observableInterfaceType)$observableReturnType
move-result-object v$urlRegister
return-object v$urlRegister
:skip_sanitization
nop
""" """
) )
} }