mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-17 21:39:49 -04:00
Show banner alerting users to how google developer verification threatens F-Droid.
This commit is contained in:
committed by
Torsten Grote
parent
8308bdc830
commit
efa73efda6
@@ -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>
|
||||
|
||||
@@ -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, "[]"));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user