diff --git a/extensions/strava/src/main/java/app/revanced/extension/strava/AddRouteExportPatch.java b/extensions/strava/src/main/java/app/revanced/extension/strava/AddRouteExportPatch.java new file mode 100644 index 000000000..3f81d5b6a --- /dev/null +++ b/extensions/strava/src/main/java/app/revanced/extension/strava/AddRouteExportPatch.java @@ -0,0 +1,88 @@ +package app.revanced.extension.strava; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import app.revanced.extension.shared.Utils; + +public final class AddRouteExportPatch { + + public static void showExportDialog(Activity activity) { + try { + Intent intent = activity.getIntent(); + if (intent == null) return; + + Bundle extras = intent.getExtras(); + if (extras == null) return; + + String routeId = null; + + // Scan all Intent extras to find the Route ID. + for (String key : extras.keySet()) { + Object val = extras.get(key); + if (val != null) { + String strVal = val.toString(); + + // Check if we have a "SavedRoute" or "SuggestedRoute" object. + if (val.getClass().getName().contains("SavedRoute")) { + try { + for (java.lang.reflect.Field field : val.getClass().getDeclaredFields()) { + field.setAccessible(true); + if (field.getType() == long.class) { + long id = field.getLong(val); + if (id > 0) { + routeId = String.valueOf(id); + break; + } + } + } + } catch (Exception e) {} + } + + if (routeId != null) break; + + // Fallback: regex search in the text content. + Matcher m = Pattern.compile("routes/([0-9]+)").matcher(strVal); + if (m.find()) { + routeId = m.group(1); + break; + } + } + } + + if (routeId != null) { + final String finalRouteId = routeId; + final String finalUrl = "https://www.strava.com/routes/" + finalRouteId; + + // Wait 500ms to allow Strava's BottomSheet to open, + // then display the popup on top of it. + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (activity.isDestroyed() || activity.isFinishing()) return; + + new AlertDialog.Builder(activity) + .setTitle("Share options") + .setMessage("Do you want to export this route or use the share menu?") + .setPositiveButton("Export GPX", (dialog, which) -> { + Utils.openLink(finalUrl + "/export_gpx"); + activity.finish(); + }) + .setNeutralButton("Export TCX", (dialog, which) -> { + Utils.openLink(finalUrl + "/export_tcx"); + activity.finish(); + }) + .setNegativeButton("Share", (dialog, which) -> { + // Close the popup, Strava's share menu is already displayed underneath. + }) + .show(); + }, 500); + } + } catch (Exception e) { + // Silently ignore errors to avoid crashes on other types of shares. + } + } +} diff --git a/extensions/strava/stub/src/main/java/com/strava/routes/data/Route.java b/extensions/strava/stub/src/main/java/com/strava/routes/data/Route.java new file mode 100644 index 000000000..1a8fee4a4 --- /dev/null +++ b/extensions/strava/stub/src/main/java/com/strava/routes/data/Route.java @@ -0,0 +1,9 @@ +package com.strava.routes.data; + +import java.io.Serializable; + +public class Route implements Serializable { + public String getId() { + return null; + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/route/export/AddRouteExportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/route/export/AddRouteExportPatch.kt new file mode 100644 index 000000000..bc2a3f51a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/route/export/AddRouteExportPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.strava.route.export + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.strava.misc.extension.sharedExtensionPatch + +private const val ROUTE_EXPORT_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/AddRouteExportPatch;" + +@Suppress("unused") +val addRouteExportPatch = bytecodePatch( + name = "Add route export", + description = "Extends the route menu with items to export them in GPX or TCX formats.", +) { + compatibleWith("com.strava") + + dependsOn(sharedExtensionPatch) + + apply { + // Intercept onCreate to display the export dialog over. + shareSheetActivityOnCreateMethod.addInstructions( + 0, + "invoke-static {p0}, $ROUTE_EXPORT_CLASS_DESCRIPTOR->showExportDialog(Landroid/app/Activity;)V" + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/route/export/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/route/export/Fingerprints.kt new file mode 100644 index 000000000..029659a1d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/route/export/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.strava.route.export + +import app.revanced.patcher.definingClass +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.name +import app.revanced.patcher.patch.BytecodePatchContext + +internal val BytecodePatchContext.shareSheetActivityOnCreateMethod by gettingFirstMethodDeclaratively { + name("onCreate") + definingClass("Lcom/strava/sharing/view/ShareSheetActivity;") +}