Add confirmation gate for external app-listing deep links

Ad networks can exploit Aurora's market:// and play.google.com VIEW
intent-filters to launch the app straight into a listing without user
intent (issue #1450). Route these external deep links through a new
translucent DeepLinkConfirmActivity that shows a Play Store-style
bottom sheet over the launching app; the listing only opens after an
explicit tap, defeating auto-launch regardless of referrer.

- DeepLinkConfirmActivity: resolves the listing, shows the sheet, then
  forwards to ComposeActivity via the Screen parcel (Open) or finishes
  (Cancel). Forwards directly when the user opts out.
- DeepLinkConfirmSheet: bottom sheet matching the existing sheets/
  convention; shows the target id and the launching app when known.
- Move the VIEW intent-filters off ComposeActivity onto the new
  activity, using a transparent edge-to-edge translucent theme.
- Add a "Confirm external links" toggle in Security (default on).
This commit is contained in:
Rahul Patel
2026-05-31 01:47:24 +05:30
parent cb682f1f0b
commit f1064ceab9
8 changed files with 295 additions and 2 deletions

View File

@@ -103,6 +103,15 @@
<intent-filter>
<action android:name="android.intent.action.SHOW_APP_INFO" />
</intent-filter>
</activity>
<!-- Confirmation gate for external app-listing deep links (market:// & play.google.com) -->
<activity
android:name=".DeepLinkConfirmActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Translucent.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -121,7 +130,6 @@
<data android:scheme="https" />
<data android:host="market.android.com" />
<data android:host="play.google.com" />
<data android:host="play.google.com" />
</intent-filter>
</activity>

View File

@@ -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 }

View File

@@ -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 -> ""
}
}

View File

@@ -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) }
)
}
)
}
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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) {

View File

@@ -191,6 +191,11 @@
<string name="app_lock_unlock">Unlock</string>
<string name="app_lock_prompt_title">Unlock Aurora Store</string>
<string name="app_lock_prompt_subtitle">Enter phone screen lock pattern, PIN, password or fingerprint</string>
<string name="confirm_deeplink_title">Confirm external links</string>
<string name="confirm_deeplink_summary">Ask before opening an app listing from a link, preventing ads from launching Aurora Store without your consent.</string>
<string name="confirm_deeplink_sheet_title">Continue to Aurora Store?</string>
<string name="confirm_deeplink_sheet_message">An external app wants to open Aurora Store</string>
<string name="confirm_deeplink_sheet_source">Opened from %1$s</string>
<string name="installer_service_available">"Aurora Services is available and ready to install."</string>
<string name="installer_service_unavailable">Install Aurora Services 1.0.9 or above, or change the installer.</string>
<string name="installer_service_misconfigured">Set up Aurora Services and grant all permissions first.</string>

View File

@@ -35,6 +35,14 @@
<item name="android:windowTranslucentNavigation">true</item>
</style>
<!-- Translucent variant with edge-to-edge transparent system bars (no status bar scrim) -->
<style name="AppTheme.Translucent.Transparent">
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="AppTheme.BottomSheetStyle" parent="Widget.Material3.BottomSheet" />
<style name="Chip.Filter" parent="@style/Widget.Material3.Chip.Filter" />