mirror of
https://github.com/whyorean/AuroraStore.git
synced 2026-06-11 01:06:05 -04:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
105
app/src/main/java/com/aurora/store/DeepLinkConfirmActivity.kt
Normal file
105
app/src/main/java/com/aurora/store/DeepLinkConfirmActivity.kt
Normal 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 -> ""
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user