diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index 88139c8fd..228b02286 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -80,6 +80,7 @@ public class GlobalUserPreferences{ public static boolean mentionRebloggerAutomatically; public static boolean showPostsWithoutAlt; public static boolean showMediaPreview; + public static boolean removeTrackingParams; public static SharedPreferences getPrefs(){ return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); @@ -160,6 +161,7 @@ public class GlobalUserPreferences{ mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false); showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true); showMediaPreview=prefs.getBoolean("showMediaPreview", true); + removeTrackingParams=prefs.getBoolean("removeTrackingParams", true); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; @@ -234,6 +236,7 @@ public class GlobalUserPreferences{ .putBoolean("enableDeleteNotifications", enableDeleteNotifications) .putBoolean("showPostsWithoutAlt", showPostsWithoutAlt) .putBoolean("showMediaPreview", showMediaPreview) + .putBoolean("removeTrackingParams", removeTrackingParams) .apply(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index e65c9bba7..0c87e48e9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -99,6 +99,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan; import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.SimpleTextWatcher; +import org.joinmastodon.android.utils.Tracking; import org.joinmastodon.android.utils.TransferSpeedTracker; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController; @@ -1175,6 +1176,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void actuallyPublish(boolean preview){ String text=mainEditText.getText().toString(); + if(GlobalUserPreferences.removeTrackingParams) + text=Tracking.cleanUrlsInText(text); CreateStatus.Request req=new CreateStatus.Request(); if("bottom".equals(postLang.encoding)){ text=new StatusTextEncoder(Bottom::encode).encode(text); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsPrivacyFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsPrivacyFragment.java index e4e097057..fab0e373c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsPrivacyFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsPrivacyFragment.java @@ -25,7 +25,7 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment{ private Instance instance; //MOSHIDON - private CheckableListItem unlistedRepliesItem; + private CheckableListItem unlistedRepliesItem, removeTrackingParams; @Override @@ -38,7 +38,8 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment{ privacy=self.source.privacy; onDataLoaded(List.of( privacyItem=new ListItem<>(R.string.sk_settings_default_visibility, getPrivacyString(privacy), R.drawable.ic_fluent_eye_24_regular, this::onPrivacyClick, 0, false), - unlistedRepliesItem=new CheckableListItem<>(R.string.mo_change_default_reply_visibility_to_unlisted, R.string.mo_setting_default_reply_privacy_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.defaultToUnlistedReplies, R.drawable.ic_fluent_lock_open_24_regular, i->toggleCheckableItem(unlistedRepliesItem), true), + unlistedRepliesItem=new CheckableListItem<>(R.string.mo_change_default_reply_visibility_to_unlisted, R.string.mo_setting_default_reply_privacy_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.defaultToUnlistedReplies, R.drawable.ic_fluent_lock_open_24_regular, i->toggleCheckableItem(unlistedRepliesItem)), + removeTrackingParams=new CheckableListItem<>(R.string.mo_settings_remove_tracking_params, R.string.mo_settings_remove_tracking_params_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.removeTrackingParams, R.drawable.ic_fluent_eye_tracking_off_24_filled, i->toggleCheckableItem(removeTrackingParams), true), lockedItem=new CheckableListItem<>(R.string.sk_settings_lock_account, 0, CheckableListItem.Style.SWITCH, self.locked, R.drawable.ic_fluent_person_available_24_regular, i->toggleCheckableItem(lockedItem)) )); @@ -89,6 +90,7 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment{ public void onPause(){ super.onPause(); GlobalUserPreferences.defaultToUnlistedReplies=unlistedRepliesItem.checked; + GlobalUserPreferences.removeTrackingParams=removeTrackingParams.checked; GlobalUserPreferences.save(); AccountSession s=AccountSessionManager.get(accountID); Account self=s.self; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 7f79613f2..59e604952 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -125,6 +125,7 @@ import org.joinmastodon.android.ui.sheets.BlockAccountConfirmationSheet; import org.joinmastodon.android.ui.sheets.MuteAccountConfirmationSheet; import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.utils.Tracking; import org.parceler.Parcels; import java.io.File; @@ -196,6 +197,8 @@ public class UiUtils { } public static void launchWebBrowser(Context context, String url) { + if(GlobalUserPreferences.removeTrackingParams) + url=Tracking.removeTrackingParameters(url); try { if (GlobalUserPreferences.useCustomTabs) { new CustomTabsIntent.Builder() @@ -1480,6 +1483,8 @@ public class UiUtils { } public static void copyText(View v, String text) { + if(GlobalUserPreferences.removeTrackingParams) + text=Tracking.cleanUrlsInText(text); Context context = v.getContext(); context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text)); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()) { // Android 13+ SystemUI shows its own thing when you put things into the clipboard diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/Tracking.java b/mastodon/src/main/java/org/joinmastodon/android/utils/Tracking.java new file mode 100644 index 000000000..93fb6aab2 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/Tracking.java @@ -0,0 +1,107 @@ +package org.joinmastodon.android.utils; + +import android.net.Uri; +import android.util.Patterns; + +import androidx.annotation.NonNull; + +import java.util.Arrays; +import java.util.regex.Matcher; + +// Inspired by https://github.com/GeopJr/Tuba/blob/91a036edff9ab1ffb38d5b54a33023e5db551051/src/Utils/Tracking.vala + +public class Tracking{ + /* https://github.com/brave/brave-core/blob/face8d58ab81422480c8c05b9ba5d518e1a2d227/components/query_filter/utils.cc#L23-L119 */ + private static final String[] TRACKING_IDS={ + // Strip any utm_ based ones + "utm_", + // https://github.com/brave/brave-browser/issues/4239 + "fbclid", "gclid", "msclkid", "mc_eid", + // New Facebook one + "mibexid", + // https://github.com/brave/brave-browser/issues/9879 + "dclid", + // https://github.com/brave/brave-browser/issues/13644 + "oly_anon_id", "oly_enc_id", + // https://github.com/brave/brave-browser/issues/11579 + "_openstat", + // https://github.com/brave/brave-browser/issues/11817 + "vero_conv", "vero_id", + // https://github.com/brave/brave-browser/issues/13647 + "wickedid", + // https://github.com/brave/brave-browser/issues/11578 + "yclid", + // https://github.com/brave/brave-browser/issues/8975 + "__s", + // https://github.com/brave/brave-browser/issues/17451 + "rb_clickid", + // https://github.com/brave/brave-browser/issues/17452 + "s_cid", + // https://github.com/brave/brave-browser/issues/17507 + "ml_subscriber", "ml_subscriber_hash", + // https://github.com/brave/brave-browser/issues/18020 + "twclid", + // https://github.com/brave/brave-browser/issues/18758 + "gbraid", "wbraid", + // https://github.com/brave/brave-browser/issues/9019 + "_hsenc", "__hssc", "__hstc", "__hsfp", "hsCtaTracking", + // https://github.com/brave/brave-browser/issues/22082 + "oft_id", "oft_k", "oft_lk", "oft_d", "oft_c", "oft_ck", "oft_ids", "oft_sk", + // https://github.com/brave/brave-browser/issues/11580 + "igshid", + // Instagram Threads + "ad_id", "adset_id", "campaign_id", "ad_name", "adset_name", "campaign_name", "placement", + // Reddit + "share_id", "ref", "ref_share", + }; + + /** + * Tries to remove tracking parameters from a URL. + * + * @param url The original URL with tracking parameters + * @return The URL with the tracking parameters removed. + */ + @NonNull + public static String removeTrackingParameters(@NonNull String url){ + Uri uri=Uri.parse(url); + if(uri==null) + return url; + Uri.Builder uriBuilder=uri.buildUpon().clearQuery(); + + // Iterate over existing parameters and add them back if they are not tracking parameters + for(String paramName : uri.getQueryParameterNames()){ + if(!isTrackingParameter(paramName)){ + for(String paramValue : uri.getQueryParameters(paramName)){ + uriBuilder.appendQueryParameter(paramName, paramValue); + } + } + } + + return uriBuilder.build().toString(); + } + + /** + * Cleans URLs within the provided text, removing the tracking parameters from them. + * + * @param text The text that may contain URLs. + * @return The given text with cleaned URLs. + */ + public static String cleanUrlsInText(String text){ + Matcher matcher=Patterns.WEB_URL.matcher(text); + StringBuffer sb=new StringBuffer(); + + while(matcher.find()){ + String url=matcher.group(); + matcher.appendReplacement(sb, removeTrackingParameters(url)); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Returns true if the given parameter is used for tracking. + */ + private static boolean isTrackingParameter(String parameter){ + return Arrays.stream(TRACKING_IDS).anyMatch(trackingId->parameter.toLowerCase().contains(trackingId)); + } +} \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_eye_tracking_off_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_eye_tracking_off_24_filled.xml new file mode 100644 index 000000000..06e3a0b90 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_eye_tracking_off_24_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/values/strings_mo.xml b/mastodon/src/main/res/values/strings_mo.xml index 3276df793..4131bf125 100644 --- a/mastodon/src/main/res/values/strings_mo.xml +++ b/mastodon/src/main/res/values/strings_mo.xml @@ -97,6 +97,7 @@ Vibrate when interacting with posts Bookmark or reblog posts from the notification Posts will be hidden in all timelines, but can be revealed in threads and notifications + Strip tracking information from links Notification Audience @@ -117,6 +118,7 @@ No UnifiedPush Distributors installed. You will not receive any notifications. UnifiedPush is not enabled. You will not receive any notifications. Enable + Private Links Muted accounts