diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0bf3e078d..e9bcc3f1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -103,6 +103,15 @@
+
+
+
+
@@ -121,7 +130,6 @@
-
diff --git a/app/src/main/java/com/aurora/store/ComposeActivity.kt b/app/src/main/java/com/aurora/store/ComposeActivity.kt
index b98e6b6af..5ca6de392 100644
--- a/app/src/main/java/com/aurora/store/ComposeActivity.kt
+++ b/app/src/main/java/com/aurora/store/ComposeActivity.kt
@@ -152,7 +152,7 @@ class ComposeActivity : FragmentActivity() {
}
private fun resolveStartDestination(): Screen {
- // Parcel-based navigation (e.g. from NotificationUtil)
+ // Parcel-based navigation (e.g. from NotificationUtil or DeepLinkConfirmActivity)
IntentCompat.getParcelableExtra(intent, Screen.PARCEL_KEY, Screen::class.java)
?.let { return it }
diff --git a/app/src/main/java/com/aurora/store/DeepLinkConfirmActivity.kt b/app/src/main/java/com/aurora/store/DeepLinkConfirmActivity.kt
new file mode 100644
index 000000000..49ef5f3b4
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/DeepLinkConfirmActivity.kt
@@ -0,0 +1,105 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Aurora OSS
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.aurora.store
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.fragment.app.FragmentActivity
+import com.aurora.store.compose.navigation.Screen
+import com.aurora.store.compose.theme.AuroraTheme
+import com.aurora.store.compose.ui.sheets.DeepLinkConfirmSheet
+import com.aurora.store.util.Preferences
+
+/**
+ * Translucent trampoline that gates external [Intent.ACTION_VIEW] app/developer listing deep links
+ * (market:// and play.google.com links). These are the vector ads exploit to launch Aurora into a
+ * listing without intent, so a Play Store-style confirmation sheet is shown floating over the
+ * launching app before forwarding to [ComposeActivity]. When the user has opted out, or the intent
+ * doesn't resolve to a listing, it forwards immediately without prompting.
+ */
+class DeepLinkConfirmActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ val target = resolveDeepLink()
+ val shouldConfirm = target != null &&
+ Preferences.getBoolean(this, Preferences.PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK, true)
+
+ if (!shouldConfirm) {
+ forwardToAurora(target)
+ return
+ }
+
+ val referrerLabel = resolveReferrerLabel()
+ setContent {
+ AuroraTheme {
+ DeepLinkConfirmSheet(
+ targetLabel = target.deepLinkLabel(),
+ sourceLabel = referrerLabel,
+ onOpen = { forwardToAurora(target) },
+ onDismiss = { finish() }
+ )
+ }
+ }
+ }
+
+ /**
+ * Resolves the listing requested by the incoming ACTION_VIEW intent, or null when the intent
+ * carries no app/developer id.
+ */
+ private fun resolveDeepLink(): Screen? {
+ if (intent.action != Intent.ACTION_VIEW) return null
+
+ val data = intent.data
+ val path = data?.path.orEmpty()
+ val id = data?.getQueryParameter("id") ?: return null
+ return when {
+ path.contains("/apps/dev") -> Screen.DevProfile(id)
+ else -> Screen.AppDetails(id)
+ }
+ }
+
+ /**
+ * Best-effort human-readable name of the app that fired the intent, derived from the activity
+ * referrer. Resolves an android-app:// referrer to its app label, falling back to the raw host.
+ * Returns null when no referrer is available.
+ */
+ private fun resolveReferrerLabel(): String? {
+ val ref = referrer ?: return null
+ val pkg = if (ref.scheme == "android-app") ref.host else null
+ if (pkg != null) {
+ return runCatching {
+ packageManager.getApplicationLabel(
+ packageManager.getApplicationInfo(pkg, 0)
+ ).toString()
+ }.getOrDefault(pkg)
+ }
+ return ref.host ?: ref.toString()
+ }
+
+ private fun forwardToAurora(target: Screen?) {
+ startActivity(
+ Intent(this, ComposeActivity::class.java).apply {
+ target?.let { putExtra(Screen.PARCEL_KEY, it) }
+ // Start ComposeActivity fresh so the parcel is honoured even when Aurora is already
+ // running; without this a reused instance keeps its current screen. Mirrors the
+ // deep-link PendingIntents in NotificationUtil.
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ )
+ finish()
+ }
+
+ private fun Screen.deepLinkLabel(): String = when (this) {
+ is Screen.AppDetails -> packageName
+ is Screen.DevProfile -> developerId
+ else -> ""
+ }
+}
diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt
index 134fff43a..77fa2ac5c 100644
--- a/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt
+++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/security/SecurityPreferenceScreen.kt
@@ -30,6 +30,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.util.AppLockAuthenticator
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_APP_LOCK_ENABLED
+import com.aurora.store.util.Preferences.PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK
import com.aurora.store.util.save
@Composable
@@ -43,6 +44,11 @@ private fun ScreenContent() {
var appLockEnabled by remember {
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_APP_LOCK_ENABLED, false))
}
+ var confirmDeepLink by remember {
+ mutableStateOf(
+ Preferences.getBoolean(context, PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK, true)
+ )
+ }
fun setAppLock(enabled: Boolean) {
// Refuse to enable the lock when the device has no biometric or screen-lock to fall back on
@@ -54,6 +60,11 @@ private fun ScreenContent() {
context.save(PREFERENCE_APP_LOCK_ENABLED, enabled)
}
+ fun setConfirmDeepLink(enabled: Boolean) {
+ confirmDeepLink = enabled
+ context.save(PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK, enabled)
+ }
+
Scaffold(
topBar = { TopAppBar(title = stringResource(R.string.title_security)) }
) { paddingValues ->
@@ -75,6 +86,20 @@ private fun ScreenContent() {
}
)
}
+
+ item {
+ ListItem(
+ modifier = Modifier.clickable { setConfirmDeepLink(!confirmDeepLink) },
+ headlineContent = { Text(stringResource(R.string.confirm_deeplink_title)) },
+ supportingContent = { Text(stringResource(R.string.confirm_deeplink_summary)) },
+ trailingContent = {
+ Switch(
+ checked = confirmDeepLink,
+ onCheckedChange = { setConfirmDeepLink(it) }
+ )
+ }
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/aurora/store/compose/ui/sheets/DeepLinkConfirmSheet.kt b/app/src/main/java/com/aurora/store/compose/ui/sheets/DeepLinkConfirmSheet.kt
new file mode 100644
index 000000000..f8d91b2d0
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/ui/sheets/DeepLinkConfirmSheet.kt
@@ -0,0 +1,140 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Aurora OSS
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.aurora.store.compose.ui.sheets
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.aurora.store.R
+
+/**
+ * Play Store-style bottom sheet shown before opening an app/developer listing from an external
+ * deep link. Requires an explicit tap so ads cannot silently launch Aurora into a listing.
+ *
+ * @param targetLabel Package name or developer id the link points to
+ * @param sourceLabel Human-readable name of the app that fired the link, or null if unknown
+ * @param onOpen Invoked when the user confirms opening the listing
+ * @param onDismiss Invoked when the user cancels or dismisses the sheet
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DeepLinkConfirmSheet(
+ targetLabel: String,
+ sourceLabel: String?,
+ onOpen: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = dimensionResource(R.dimen.spacing_medium),
+ vertical = dimensionResource(R.dimen.spacing_xsmall)
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(
+ dimensionResource(R.dimen.spacing_medium)
+ )
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_logo_alt),
+ contentDescription = null,
+ modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))
+ )
+ Text(
+ text = stringResource(R.string.confirm_deeplink_sheet_title),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = dimensionResource(R.dimen.spacing_xsmall))
+ )
+
+ Column(
+ modifier = Modifier.padding(
+ horizontal = dimensionResource(R.dimen.spacing_medium),
+ vertical = dimensionResource(R.dimen.spacing_xsmall)
+ ),
+ verticalArrangement = Arrangement.spacedBy(
+ dimensionResource(R.dimen.spacing_xsmall)
+ )
+ ) {
+ Text(
+ text = stringResource(R.string.confirm_deeplink_sheet_message),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ Text(
+ text = targetLabel,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ if (!sourceLabel.isNullOrBlank()) {
+ Text(
+ text = stringResource(R.string.confirm_deeplink_sheet_source, sourceLabel),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = dimensionResource(R.dimen.spacing_xsmall),
+ vertical = dimensionResource(R.dimen.spacing_xsmall)
+ ),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onDismiss) {
+ Text(text = stringResource(R.string.action_cancel))
+ }
+ TextButton(onClick = onOpen) {
+ Text(text = stringResource(R.string.action_open))
+ }
+ }
+
+ Spacer(Modifier.navigationBarsPadding())
+ }
+ }
+}
diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt
index e01de6560..8741e82c4 100644
--- a/app/src/main/java/com/aurora/store/util/Preferences.kt
+++ b/app/src/main/java/com/aurora/store/util/Preferences.kt
@@ -81,6 +81,8 @@ object Preferences {
const val PREFERENCE_APP_LOCK_ENABLED = "PREFERENCE_APP_LOCK_ENABLED"
const val PREFERENCE_APP_LOCK_TIMEOUT = "PREFERENCE_APP_LOCK_TIMEOUT"
+ const val PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK = "PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK"
+
private var prefs: SharedPreferences? = null
fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fe74bacc8..571357842 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -191,6 +191,11 @@
Unlock
Unlock Aurora Store
Enter phone screen lock pattern, PIN, password or fingerprint
+ Confirm external links
+ Ask before opening an app listing from a link, preventing ads from launching Aurora Store without your consent.
+ Continue to Aurora Store?
+ An external app wants to open Aurora Store
+ Opened from %1$s
"Aurora Services is available and ready to install."
Install Aurora Services 1.0.9 or above, or change the installer.
Set up Aurora Services and grant all permissions first.
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index cc9850a12..f5a9b060e 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -35,6 +35,14 @@
- true
+
+
+