Show banner alerting users to how google developer verification threatens F-Droid.

This commit is contained in:
Peter Serwylo
2026-02-17 22:39:07 +00:00
committed by Torsten Grote
parent 8308bdc830
commit efa73efda6
8 changed files with 212 additions and 0 deletions

View File

@@ -820,4 +820,6 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="expand">Expand</string>
<string name="collapse">Collapse</string>
<string name="call_to_action_keepandroidopen">F-Droid is under threat. Google is changing the way you install apps on your phone. We need your help.</string>
<string name="call_to_action_keepandroidopen_url">https://keepandroidopen.org</string>
</resources>

View File

@@ -115,6 +115,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
public static final String PREF_USE_IPFS_GATEWAYS = "useIpfsGateways";
public static final String PREF_IPFSGW_DISABLED_DEFAULTS_LIST = "ipfsGwDisabledDefaultsList";
public static final String PREF_IPFSGW_USER_LIST = "ipfsGwUserList";
public static final String PREF_CALL_TO_ACTION_DISMISSED = "callToActionDismissed";
public static final String PREF_EXPERT = "expert";
public static final String PREF_FORCE_OLD_INDEX = "forceOldIndex";
public static final String PREF_FORCE_OLD_INSTALLER = "forceOldInstaller";
@@ -716,6 +717,14 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
).apply();
}
public boolean getCallToActionDismissed() {
return preferences.getBoolean(PREF_CALL_TO_ACTION_DISMISSED, false);
}
public void setCallToActionDismissed() {
preferences.edit().putBoolean(PREF_CALL_TO_ACTION_DISMISSED, true).apply();
}
public List<String> getIpfsGwDisabledDefaults() {
return Utils.parseJsonStringArray(preferences.getString(PREF_IPFSGW_DISABLED_DEFAULTS_LIST, "[]"));
}

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.unit.Dp
import androidx.core.view.DisplayCompat
import androidx.core.view.ViewCompat
@@ -89,6 +90,15 @@ fun PaddingValues.copy(
)
}
// Duplicated from the `app` module.
fun UriHandler.openUriSafe(uri: String) {
try {
openUri(uri)
} catch (e: Exception) {
Log.e("UriHandler", "Error opening $uri ", e)
}
}
class UiUtils {
companion object {
/**

View File

@@ -0,0 +1,154 @@
package org.fdroid.fdroid.views
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import org.fdroid.fdroid.Preferences
import org.fdroid.fdroid.R
import org.fdroid.fdroid.openUriSafe
import org.fdroid.fdroid.ui.theme.FDroidContent
import java.util.Calendar
/**
* Expire at the start of September 2026, to align with the countdown on keepandroidopen.org.
*
* The call to action regarding Google Developer Verification and keepandroidopen.org should not
* persist forever. Rather, we will only show it for a month or two.
* This is in the vain hope that something changes and Google decides to let us install the software
* we choose on our own device. If that were to happen, we don't want to keep showing this.
*
* @param now Allow this to be passed in for testing purposes.
*/
fun hasCallToActionExpired(now: Long = System.currentTimeMillis()): Boolean {
val calendar = Calendar.getInstance().apply {
set(2026, 8, 1) // 8 = Sept (0-based month)
}
val expiry = calendar.timeInMillis
val hasExpired = now > expiry
return hasExpired
}
/**
* Broadcast a call to action message to end users.
*
* This should only be used in exceptional circumstances. For example, it was first
* introduced to bring users attention to the Developer verification mandate which
* threatens the very existence of F-Droid.
*/
@Composable
fun CallToActionBanner(dismissed: Boolean, onDismiss: () -> Unit) {
var showBanner by remember { mutableStateOf(!dismissed) }
val uriHandler = LocalUriHandler.current
val url = stringResource(R.string.call_to_action_keepandroidopen_url)
val handleDismiss = {
onDismiss()
showBanner = false
}
AnimatedVisibility(
visible = showBanner,
exit = slideOutVertically(),
) {
Row {
Row(
Modifier
.background(colorResource(R.color.call_to_action_banner__background)),
horizontalArrangement = spacedBy(0.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.selectable(
selected = false,
onClick = { uriHandler.openUriSafe(url) },
)
.padding(start = 6.dp, top = 6.dp, bottom = 6.dp),
) {
Text(
text = stringResource(R.string.call_to_action_keepandroidopen),
textAlign = TextAlign.Center,
color = colorResource(R.color.call_to_action_banner__text),
style = TextStyle(lineHeight = 1.em),
)
Text(
text = "https://keepandroidopen.org",
textAlign = TextAlign.Center,
color = colorResource(R.color.call_to_action_banner__link),
textDecoration = TextDecoration.Underline,
// For some reason, setting the TextStyle on the above Text() makes the font size change.
// To make this have the same font, we add this empty TextStyle.
style = TextStyle(),
)
}
IconButton(onClick = handleDismiss) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = colorResource(R.color.call_to_action_banner__text),
)
}
}
}
}
}
/**
* A helper method to show [CallToActionBanner] from Java code.
*/
fun setContentCallToActionBanner(composeView: ComposeView) {
val dismissed = hasCallToActionExpired() || Preferences.get().callToActionDismissed
val onDismiss = {
Preferences.get().setCallToActionDismissed()
}
composeView.setContent {
FDroidContent {
CallToActionBanner(
dismissed = dismissed,
onDismiss = onDismiss,
)
}
}
}
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
fun CallToActionBannerPreview() {
FDroidContent(pureBlack = true) {
CallToActionBanner(onDismiss = {}, dismissed = false)
}
}

View File

@@ -7,6 +7,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.compose.ui.platform.ComposeView;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -29,6 +30,7 @@ import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.panic.HidingManager;
import org.fdroid.fdroid.views.CallToActionBannerKt;
import org.fdroid.fdroid.views.apps.AppListActivity;
import java.util.Arrays;
@@ -68,6 +70,9 @@ class LatestViewBinder implements Observer<List<AppOverviewItem>>, ChangeListene
View latestView = activity.getLayoutInflater().inflate(R.layout.main_tab_latest, parent, true);
ComposeView callToActionBanner = latestView.findViewById(R.id.call_to_action_banner);
CallToActionBannerKt.setContentCallToActionBanner(callToActionBanner);
latestAdapter = new LatestAdapter(activity);
GridLayoutManager layoutManager = new GridLayoutManager(activity, 2);

View File

@@ -15,6 +15,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/call_to_action_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<org.fdroid.fdroid.views.StatusBanner
style="@style/StatusBanner"
android:layout_width="match_parent"

View File

@@ -82,4 +82,8 @@
<color name="md_theme_surfaceContainer">#ECEDF6</color>
<color name="md_theme_surfaceContainerHigh">#E6E8F0</color>
<color name="md_theme_surfaceContainerHighest">#E0E2EA</color>
<color name="call_to_action_banner__background">#FFB9B9</color>
<color name="call_to_action_banner__text">#540303</color>
<color name="call_to_action_banner__link">#0000ff</color>
</resources>

View File

@@ -0,0 +1,23 @@
package org.fdroid.fdroid.views
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.Calendar
class CallToActionBannerTest {
@Test
fun testCallToActionExpiry() {
val notExpired = Calendar.getInstance().apply {
set(2026, 1, 1)
}.timeInMillis
assertFalse(hasCallToActionExpired(now = notExpired))
val expired = Calendar.getInstance().apply {
set(2026, 11, 1)
}.timeInMillis
assertTrue(hasCallToActionExpired(now = expired))
}
}