Merge dev

This commit is contained in:
oSumAtrIX 2026-01-30 18:01:01 +01:00
commit 48b3a2d18c
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
183 changed files with 62850 additions and 59577 deletions

View file

@ -0,0 +1 @@
<manifest/>

View file

@ -0,0 +1,217 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import app.revanced.extension.shared.ResourceType;
import com.strava.mediamodels.data.MediaType;
import com.strava.photos.data.Media;
import okhttp3.*;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import app.revanced.extension.shared.Utils;
@SuppressLint("NewApi")
public final class AddMediaDownloadPatch {
public static final int ACTION_DOWNLOAD = -1;
public static final int ACTION_OPEN_LINK = -2;
public static final int ACTION_COPY_LINK = -3;
private static final OkHttpClient client = new OkHttpClient();
public static boolean handleAction(int actionId, Media media) {
String url = getUrl(media);
switch (actionId) {
case ACTION_DOWNLOAD:
String name = media.getId();
if (media.getType() == MediaType.VIDEO) {
downloadVideo(url, name);
} else {
downloadPhoto(url, name);
}
return true;
case ACTION_OPEN_LINK:
Utils.openLink(url);
return true;
case ACTION_COPY_LINK:
copyLink(url);
return true;
default:
return false;
}
}
public static void copyLink(CharSequence url) {
Utils.setClipboard(url);
showInfoToast("link_copied_to_clipboard", "🔗");
}
public static void downloadPhoto(String url, String name) {
showInfoToast("loading", "");
Utils.runOnBackgroundThread(() -> {
try (Response response = fetch(url)) {
ResponseBody body = response.body();
String mimeType = body.contentType().toString();
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
ContentResolver resolver = Utils.getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, name + '.' + extension);
values.put(MediaStore.Images.Media.IS_PENDING, 1);
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Strava");
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
: MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri row = resolver.insert(collection, values);
try (OutputStream outputStream = resolver.openOutputStream(row)) {
transferTo(body.byteStream(), outputStream);
} finally {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_image_success", "✔️");
} catch (IOException e) {
showErrorToast("download_failure", "", e);
}
});
}
/**
* Downloads a video in the M3U8 / HLS (HTTP Live Streaming) format.
*/
public static void downloadVideo(String url, String name) {
// The first request yields multiple URLs with different stream options.
// In case of Strava, the first one is always of highest quality.
// Each stream can consist of multiple chunks.
// The second request yields the URLs of all of these chunks.
// Fetch all of them concurrently and pipe their streams into the file in order.
showInfoToast("loading", "");
Utils.runOnBackgroundThread(() -> {
try {
String highestQualityStreamUrl;
try (Response response = fetch(url)) {
highestQualityStreamUrl = replaceFileName(url, lines(response).findFirst().get());
}
List<Future<Response>> futures;
try (Response response = fetch(highestQualityStreamUrl)) {
futures = lines(response)
.map(line -> replaceFileName(highestQualityStreamUrl, line))
.map(chunkUrl -> Utils.submitOnBackgroundThread(() -> fetch(chunkUrl)))
.collect(Collectors.toList());
}
ContentResolver resolver = Utils.getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DISPLAY_NAME, name + '.' + "mp4");
values.put(MediaStore.Video.Media.IS_PENDING, 1);
values.put(MediaStore.Video.Media.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4"));
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/Strava");
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
: MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
Uri row = resolver.insert(collection, values);
try (OutputStream outputStream = resolver.openOutputStream(row)) {
Throwable error = null;
for (Future<Response> future : futures) {
if (error != null) {
if (future.cancel(true)) {
continue;
}
}
try (Response response = future.get()) {
if (error == null) {
transferTo(response.body().byteStream(), outputStream);
}
} catch (InterruptedException | IOException e) {
error = e;
} catch (ExecutionException e) {
error = e.getCause();
}
}
if (error != null) {
throw new IOException(error);
}
} finally {
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_video_success", "✔️");
} catch (IOException e) {
showErrorToast("download_failure", "", e);
}
});
}
private static String getUrl(Media media) {
return media.getType() == MediaType.VIDEO
? ((Media.Video) media).getVideoUrl()
: media.getLargestUrl();
}
private static String getString(String name, String fallback) {
int id = Utils.getResourceIdentifier(ResourceType.STRING, name);
return id != 0
? Utils.getResourceString(id)
: fallback;
}
private static void showInfoToast(String resourceName, String fallback) {
String text = getString(resourceName, fallback);
Utils.showToastShort(text);
}
private static void showErrorToast(String resourceName, String fallback, IOException exception) {
String text = getString(resourceName, fallback);
Utils.showToastLong(text + ' ' + exception.getLocalizedMessage());
}
private static Response fetch(String url) throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Got HTTP status code " + response.code());
}
return response;
}
/**
* {@code inputStream.transferTo(outputStream)} is "too new".
*/
private static void transferTo(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024 * 8];
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
}
/**
* Gets all file names.
*/
private static Stream<String> lines(Response response) {
BufferedReader reader = new BufferedReader(response.body().charStream());
return reader.lines().filter(line -> !line.startsWith("#"));
}
private static String replaceFileName(String uri, String newName) {
return uri.substring(0, uri.lastIndexOf('/') + 1) + newName;
}
}

View file

@ -0,0 +1,227 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import com.strava.modularframework.data.Destination;
import com.strava.modularframework.data.GenericLayoutModule;
import com.strava.modularframework.data.GenericModuleField;
import com.strava.modularframework.data.ListField;
import com.strava.modularframework.data.ListProperties;
import com.strava.modularframework.data.ModularComponent;
import com.strava.modularframework.data.ModularEntry;
import com.strava.modularframework.data.ModularEntryContainer;
import com.strava.modularframework.data.ModularMenuItem;
import com.strava.modularframework.data.Module;
import com.strava.modularframework.data.MultiStateFieldDescriptor;
import com.strava.modularframeworknetwork.ModularEntryNetworkContainer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@SuppressLint("NewApi")
public class HideDistractionsPatch {
public static boolean upselling;
public static boolean promo;
public static boolean followSuggestions;
public static boolean challengeSuggestions;
public static boolean joinChallenge;
public static boolean joinClub;
public static boolean activityLookback;
public static List<ModularEntry> filterChildrenEntries(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getChildrenEntries$original().stream()
.filter(childrenEntry -> !hideModularEntry(childrenEntry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return Collections.emptyList();
}
return modularEntryNetworkContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularMenuItem> filterMenuItems(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getMenuItems$original().stream()
.filter(menuItem -> !hideModularMenuItem(menuItem))
.collect(Collectors.toList());
}
public static ListProperties filterProperties(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return null;
}
return modularEntryContainer.getProperties$original();
}
public static ListProperties filterProperties(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return null;
}
return modularEntryNetworkContainer.getProperties$original();
}
public static ListField filterField(ListProperties listProperties, String key) {
ListField listField = listProperties.getField$original(key);
if (hideListField(listField)) {
return null;
}
return listField;
}
public static List<ListField> filterFields(ListField listField) {
if (hideListField(listField)) {
return null;
}
return listField.getFields$original().stream()
.filter(field -> !hideListField(field))
.collect(Collectors.toList());
}
public static List<Module> filterModules(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getModules$original().stream()
.filter(module -> !hideModule(module))
.collect(Collectors.toList());
}
public static GenericModuleField filterField(GenericLayoutModule genericLayoutModule, String key) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return null;
}
GenericModuleField field = genericLayoutModule.getField$original(key);
if (hideGenericModuleField(field)) {
return null;
}
return field;
}
public static GenericModuleField[] filterFields(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericModuleField[0];
}
return Arrays.stream(genericLayoutModule.getFields$original())
.filter(field -> !hideGenericModuleField(field))
.toArray(GenericModuleField[]::new);
}
public static GenericLayoutModule[] filterSubmodules(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericLayoutModule[0];
}
return Arrays.stream(genericLayoutModule.getSubmodules$original())
.filter(submodule -> !hideGenericLayoutModule(submodule))
.toArray(GenericLayoutModule[]::new);
}
public static List<Module> filterSubmodules(ModularComponent modularComponent) {
if (hideByName(modularComponent.getPage()) || hideByName(modularComponent.getElement())) {
return Collections.emptyList();
}
return modularComponent.getSubmodules$original().stream()
.filter(submodule -> !hideModule(submodule))
.collect(Collectors.toList());
}
public static Map<String, GenericModuleField> filterStateMap(MultiStateFieldDescriptor multiStateFieldDescriptor) {
return multiStateFieldDescriptor.getStateMap$original().entrySet().stream()
.filter(entry -> !hideGenericModuleField(entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static boolean hideModule(Module module) {
return module == null ||
hideByName(module.getPage()) ||
hideByName(module.getElement());
}
private static boolean hideModularEntry(ModularEntry modularEntry) {
return modularEntry == null ||
hideByName(modularEntry.getPage()) ||
hideByName(modularEntry.getElement()) ||
hideByDestination(modularEntry.getDestination());
}
private static boolean hideGenericLayoutModule(GenericLayoutModule genericLayoutModule) {
try {
return genericLayoutModule == null ||
hideByName(genericLayoutModule.getPage()) ||
hideByName(genericLayoutModule.getElement()) ||
hideByDestination(genericLayoutModule.getDestination());
} catch (RuntimeException getParentEntryOrThrowException) {
return false;
}
}
private static boolean hideListField(ListField listField) {
return listField == null ||
hideByName(listField.getElement()) ||
hideByDestination(listField.getDestination());
}
private static boolean hideGenericModuleField(GenericModuleField genericModuleField) {
return genericModuleField == null ||
hideByName(genericModuleField.getElement()) ||
hideByDestination(genericModuleField.getDestination());
}
private static boolean hideModularEntryContainer(ModularEntryContainer modularEntryContainer) {
return modularEntryContainer == null ||
hideByName(modularEntryContainer.getPage());
}
private static boolean hideModularEntryNetworkContainer(ModularEntryNetworkContainer modularEntryNetworkContainer) {
return modularEntryNetworkContainer == null ||
hideByName(modularEntryNetworkContainer.getPage());
}
private static boolean hideModularMenuItem(ModularMenuItem modularMenuItem) {
return modularMenuItem == null ||
hideByName(modularMenuItem.getElementName()) ||
hideByDestination(modularMenuItem.getDestination());
}
private static boolean hideByName(String name) {
return name != null && (
upselling && name.contains("_upsell") ||
promo && (name.equals("promo") || name.equals("top_of_tab_promo")) ||
followSuggestions && name.equals("suggested_follows") ||
challengeSuggestions && name.equals("suggested_challenges") ||
joinChallenge && name.equals("challenge") ||
joinClub && name.equals("club") ||
activityLookback && name.equals("highlighted_activity_lookback")
);
}
private static boolean hideByDestination(Destination destination) {
if (destination == null) {
return false;
}
String url = destination.getUrl();
return url != null && (
upselling && url.startsWith("strava://subscription/checkout")
);
}
}