├── app
├── .gitignore
├── proguard-rules.pro
├── src
│ └── main
│ │ ├── res
│ │ ├── resources.properties
│ │ ├── values
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── themes.xml
│ │ │ └── strings.xml
│ │ ├── values-night
│ │ │ └── themes.xml
│ │ ├── mipmap-anydpi
│ │ │ └── ic_launcher.xml
│ │ ├── xml
│ │ │ ├── backup_rules.xml
│ │ │ ├── data_extraction_rules.xml
│ │ │ └── network_security_config.xml
│ │ └── drawable
│ │ │ ├── ethereum.xml
│ │ │ ├── x_logo.xml
│ │ │ ├── bank.xml
│ │ │ ├── bluesky_logo.xml
│ │ │ ├── paypal.xml
│ │ │ ├── monero.xml
│ │ │ ├── zcash.xml
│ │ │ ├── outline_newsmode.xml
│ │ │ ├── crypto.xml
│ │ │ ├── github.xml
│ │ │ ├── discord_logo.xml
│ │ │ ├── litecoin.xml
│ │ │ ├── matrix_logo.xml
│ │ │ ├── bitcoin.xml
│ │ │ ├── telegram_logo.xml
│ │ │ ├── grapheneos_logo.xml
│ │ │ ├── mastodon_logo.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── donate_ethereum_qr_code.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── kotlin
│ │ └── app
│ │ │ └── grapheneos
│ │ │ └── info
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── releases
│ │ │ │ ├── ReleasesUiState.kt
│ │ │ │ ├── ReleasesScreen.kt
│ │ │ │ ├── ReleasesViewModel.kt
│ │ │ │ └── Changelog.kt
│ │ │ ├── donate
│ │ │ │ ├── cryptocurrency
│ │ │ │ │ ├── ZcashScreen.kt
│ │ │ │ │ ├── LitecoinScreen.kt
│ │ │ │ │ ├── MoneroScreen.kt
│ │ │ │ │ ├── EthereumScreen.kt
│ │ │ │ │ ├── CardanoScreen.kt
│ │ │ │ │ ├── DonateCryptoCurrencyStartScreen.kt
│ │ │ │ │ ├── BitcoinScreen.kt
│ │ │ │ │ └── AddressInfoItem.kt
│ │ │ │ ├── GithubSponsorsScreen.kt
│ │ │ │ ├── PaypalScreen.kt
│ │ │ │ ├── banktransfers
│ │ │ │ │ ├── AccountInfoItem.kt
│ │ │ │ │ └── BankTransfersScreen.kt
│ │ │ │ └── DonateStartScreen.kt
│ │ │ ├── reusablecomposables
│ │ │ │ ├── ClickableText.kt
│ │ │ │ ├── ScreenLazyColumn.kt
│ │ │ │ └── CardItem.kt
│ │ │ └── community
│ │ │ │ └── CommunityScreen.kt
│ │ │ ├── Application.kt
│ │ │ ├── preferences
│ │ │ ├── PreferencesUiState.kt
│ │ │ └── PreferencesViewModel.kt
│ │ │ └── MainActivity.kt
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── org
│ │ └── grapheneos
│ │ └── tls
│ │ └── ModernTLSSocketFactory.java
├── lint.xml
└── build.gradle.kts
├── .idea
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── AndroidProjectSystem.xml
├── misc.xml
├── gradle.xml
├── runConfigurations.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── uiDesigner.xml
└── other.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .github
├── workflows
│ ├── validate-gradle-wrapper.yml
│ └── build.yml
└── dependabot.yml
├── settings.gradle.kts
├── .gitignore
├── LICENSE
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Info/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GrapheneOS/Info/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.github/workflows/validate-gradle-wrapper.yml:
--------------------------------------------------------------------------------
1 | name: Validate Gradle Wrapper
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | validation:
7 | name: Validation
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v6
11 | - uses: gradle/actions/wrapper-validation@v5
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | target-branch: main
8 | - package-ecosystem: gradle
9 | directory: "/"
10 | schedule:
11 | interval: daily
12 | target-branch: main
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Info"
17 | include(":app")
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/deploymentTargetDropDown.xml
11 | /.idea/deploymentTargetSelector.xml
12 | .DS_Store
13 | /build
14 | /captures
15 | .externalNativeBuild
16 | .cxx
17 | keystore.properties
18 | *.keystore
19 | local.properties
20 | /releases
21 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build application
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v6
11 | - name: Set up JDK 21
12 | uses: actions/setup-java@v5
13 | with:
14 | distribution: 'temurin'
15 | java-version: 21
16 | cache: gradle
17 | - name: Build with Gradle
18 | run: ./gradlew build --no-daemon
19 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ethereum.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/x_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bank.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
20 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/Application.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info
2 |
3 | import android.net.http.HttpResponseCache
4 | import android.util.Log
5 | import java.io.File
6 | import java.io.IOException
7 |
8 | const val TAG = "Application"
9 |
10 | class Application : android.app.Application() {
11 | override fun onCreate() {
12 | super.onCreate()
13 |
14 | try {
15 | val httpCacheDir = File(applicationContext.cacheDir, "http")
16 | val httpCacheSize = (10 * 1024 * 1024).toLong() // 10 MiB
17 | HttpResponseCache.install(httpCacheDir, httpCacheSize)
18 | } catch (e: IOException) {
19 | Log.e(TAG, "HTTP response cache installation failed", e)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesUiState.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.releases
2 |
3 | import androidx.compose.runtime.mutableStateMapOf
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.SavedStateHandle
6 | import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
7 | import androidx.lifecycle.viewmodel.compose.saveable
8 |
9 | @OptIn(SavedStateHandleSaveableApi::class)
10 | class ReleasesUiState(savedStateHandle: SavedStateHandle) {
11 | var didInitialScroll: Boolean by savedStateHandle.saveable {
12 | mutableStateOf(false)
13 | }
14 | /** Unsorted release notes, use .toSortedMap().toList().asReversible() to get them in the proper order. */
15 | val entries: MutableMap = mutableStateMapOf()
16 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bluesky_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/paypal.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/preferences/PreferencesUiState.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.preferences
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import app.grapheneos.info.InfoAppScreens
8 |
9 | /** Preference pairs.
10 | * The first is the preference key, while the second is the default value. */
11 | data class PreferencesUiState(
12 | /** Whether the preferences are loaded. **/
13 | val isPreferencesLoaded: MutableState = mutableStateOf(false),
14 |
15 | /** Start destination of NavHost. **/
16 | val startDestination: Pair, MutableState> = Pair(
17 | stringPreferencesKey("START_DESTINATION"),
18 | mutableStateOf(InfoAppScreens.Community.name),
19 | ),
20 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/monero.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/zcash.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outline_newsmode.xml:
--------------------------------------------------------------------------------
1 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2024-2025 GrapheneOS
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/crypto.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | grapheneos.org
6 |
7 |
8 |
9 | C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=
10 |
11 |
12 | diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=
13 |
14 |
15 | sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=
16 |
17 |
18 | fk6IOKit1ild5647BH06ujSIq5XbCgqlbYl6ANhhi88=
19 |
20 |
21 | plxMUkaUYKzH2Pqc3qQA3lmLOf+CEuWFm0eQyws2ffM=
22 |
23 |
24 | 2P1xn0w4b0/Lp7cjEaKemkpZrJ7riBBZCapnsQNR/PA=
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/discord_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/drawable/litecoin.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/matrix_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bitcoin.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F),
32 | */
33 | )
34 |
35 | @Composable
36 | fun InfoTheme(
37 | darkTheme: Boolean = isSystemInDarkTheme(),
38 | // Dynamic color is available on Android 12+
39 | dynamicColor: Boolean = true,
40 | content: @Composable () -> Unit
41 | ) {
42 | val colorScheme = when {
43 | dynamicColor -> {
44 | val context = LocalContext.current
45 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
46 | }
47 |
48 | darkTheme -> DarkColorScheme
49 | else -> LightColorScheme
50 | }
51 |
52 | MaterialTheme(
53 | colorScheme = colorScheme,
54 | typography = Typography,
55 | content = content
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/telegram_logo.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info
2 |
3 | import android.content.Context
4 | import android.net.http.HttpResponseCache
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.compose.runtime.collectAsState
10 | import androidx.compose.runtime.getValue
11 | import androidx.datastore.core.DataStore
12 | import androidx.datastore.preferences.core.Preferences
13 | import androidx.datastore.preferences.preferencesDataStore
14 | import androidx.lifecycle.viewmodel.compose.viewModel
15 | import app.grapheneos.info.preferences.PreferencesViewModel
16 | import app.grapheneos.info.ui.theme.InfoTheme
17 | import kotlinx.coroutines.CoroutineScope
18 | import kotlinx.coroutines.Dispatchers
19 | import kotlinx.coroutines.launch
20 |
21 | val Context.dataStore: DataStore by preferencesDataStore(name = "preferences")
22 |
23 | class MainActivity : ComponentActivity() {
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | enableEdgeToEdge()
26 | super.onCreate(savedInstanceState)
27 |
28 | setContent {
29 | val preferencesViewModel: PreferencesViewModel = viewModel()
30 |
31 | val preferencesUiState by preferencesViewModel.uiState.collectAsState()
32 |
33 | InfoTheme {
34 | /** Wait for preferences to load before loading the app to avoid race conditions */
35 | if (preferencesUiState.isPreferencesLoaded.value) {
36 | InfoApp()
37 | }
38 | }
39 | }
40 | }
41 |
42 | private val cacheFlushCoroutineScope = CoroutineScope(Dispatchers.IO)
43 |
44 | override fun onStop() {
45 | super.onStop()
46 |
47 | cacheFlushCoroutineScope.launch {
48 | val cache = HttpResponseCache.getInstalled()
49 | cache?.flush()
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/grapheneos_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/ZcashScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.tooling.preview.Wallpapers
13 | import androidx.compose.ui.unit.dp
14 | import app.grapheneos.info.R
15 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
16 |
17 | @Composable
18 | fun ZcashScreen(
19 | modifier: Modifier = Modifier,
20 | showSnackbarError: (String) -> Unit = {},
21 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
22 | ) {
23 | ScreenLazyColumn(
24 | modifier = modifier
25 | .fillMaxSize(),
26 | additionalContentPadding = additionalContentPadding
27 | ) {
28 | item {
29 | Text(stringResource(R.string.zcash_info))
30 | }
31 | item {
32 | AddressInfoItem(
33 | title = "Zcash (transparent)",
34 | qrCodePainterResourceId = R.drawable.donate_zcash_transparent_qr_code,
35 | qrCodeContentDescription = stringResource(R.string.zcash_transparent_qr_code_description),
36 | addressUrl = "zcash:t1SJABjX8rqgzqgrzLW5dUw7ikSDZ2snD8A?label=GrapheneOS%20Foundation&message=Donation%20to%20GrapheneOS%20Foundation",
37 | address = "t1SJABjX8rqgzqgrzLW5dUw7ikSDZ2snD8A",
38 | showSnackbarError = showSnackbarError
39 | )
40 | }
41 | }
42 | }
43 |
44 | @Preview(
45 | showBackground = true,
46 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
47 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
48 | )
49 | @Composable
50 | private fun ZcashScreenPreview() {
51 | MaterialTheme {
52 | ZcashScreen()
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/LitecoinScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.tooling.preview.Wallpapers
13 | import androidx.compose.ui.unit.dp
14 | import app.grapheneos.info.R
15 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
16 |
17 | @Composable
18 | fun LitecoinScreen(
19 | modifier: Modifier = Modifier,
20 | showSnackbarError: (String) -> Unit = {},
21 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
22 | ) {
23 | ScreenLazyColumn(
24 | modifier = modifier
25 | .fillMaxSize(),
26 | additionalContentPadding = additionalContentPadding
27 | ) {
28 | item {
29 | Text(stringResource(R.string.litecoin_info))
30 | }
31 | item {
32 | AddressInfoItem(
33 | title = "Litecoin",
34 | qrCodePainterResourceId = R.drawable.donate_litecoin_qr_code,
35 | qrCodeContentDescription = stringResource(R.string.litecoin_qr_code_description),
36 | addressUrl = "litecoin:ltc1qzssmqueth6zjzr95rkluy5xdx9q4lk8vyrvea9?label=GrapheneOS%20Foundation&message=Donation%20to%20GrapheneOS%20Foundation",
37 | address = "ltc1qzssmqueth6zjzr95rkluy5xdx9q4lk8vyrvea9",
38 | showSnackbarError = showSnackbarError
39 | )
40 | }
41 | }
42 | }
43 |
44 | @Preview(
45 | showBackground = true,
46 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
47 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
48 | )
49 | @Composable
50 | private fun LitecoinScreenPreview() {
51 | MaterialTheme {
52 | LitecoinScreen()
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mastodon_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/MoneroScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.tooling.preview.Wallpapers
13 | import androidx.compose.ui.unit.dp
14 | import app.grapheneos.info.R
15 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
16 |
17 | @Composable
18 | fun MoneroScreen(
19 | modifier: Modifier = Modifier,
20 | showSnackbarError: (String) -> Unit = {},
21 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
22 | ) {
23 | ScreenLazyColumn(
24 | modifier = modifier
25 | .fillMaxSize(),
26 | additionalContentPadding = additionalContentPadding
27 | ) {
28 | item {
29 | Text(stringResource(R.string.monero_info))
30 | }
31 | item {
32 | AddressInfoItem(
33 | title = "Monero",
34 | qrCodePainterResourceId = R.drawable.donate_monero_qr_code,
35 | qrCodeContentDescription = stringResource(R.string.monero_qr_code_description),
36 | addressUrl = "monero:862CebHaBpFPgYoNC6zw4U8rsXrDjD8s5LMJNS7yVCRHMUKr9dDi7adMSLUMjkDYJ85xahQTCJHHyK5RCvvRJu9x7iSzN9D?recipient_name=GrapheneOS%20Foundation&tx_description=Donation%20to%20GrapheneOS%20Foundation",
37 | address = "862CebHaBpFPgYoNC6zw4U8rsXrDjD8s5LMJNS7yVCRHMUKr9dDi7adMSLUMjkDYJ85xahQTCJHHyK5RCvvRJu9x7iSzN9D",
38 | showSnackbarError = showSnackbarError
39 | )
40 | }
41 | }
42 | }
43 |
44 | @Preview(
45 | showBackground = true,
46 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
47 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
48 | )
49 | @Composable
50 | private fun MoneroScreenPreview() {
51 | MaterialTheme {
52 | MoneroScreen()
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/EthereumScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.tooling.preview.Wallpapers
14 | import androidx.compose.ui.unit.dp
15 | import app.grapheneos.info.R
16 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
17 |
18 | @Composable
19 | fun EthereumScreen(
20 | modifier: Modifier = Modifier,
21 | showSnackbarError: (String) -> Unit = {},
22 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
23 | ) {
24 | ScreenLazyColumn(
25 | modifier = modifier
26 | .fillMaxSize(),
27 | additionalContentPadding = additionalContentPadding
28 | ) {
29 | item {
30 | Text(stringResource(R.string.etherium_info))
31 | }
32 | item {
33 | AddressInfoItem(
34 | title = "Ethereum",
35 | qrCodePainterResourceId = R.drawable.donate_ethereum_qr_code,
36 | qrCodeContentDescription = stringResource(R.string.ethereum_qr_code_description),
37 | addressUrl = "ethereum:0xC822A62E5Ab443E0001f30cEB9B2336D0524fC61",
38 | address = "0xC822A62E5Ab443E0001f30cEB9B2336D0524fC61",
39 | showSnackbarError = showSnackbarError
40 | )
41 | }
42 | item {
43 | Text(
44 | stringResource(R.string.etherium_token_donation_notice),
45 | fontWeight = FontWeight.Bold
46 | )
47 | }
48 | }
49 | }
50 |
51 | @Preview(
52 | showBackground = true,
53 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
54 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
55 | )
56 | @Composable
57 | private fun EthereumScreenPreview() {
58 | MaterialTheme {
59 | EthereumScreen()
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/CardanoScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.text.selection.SelectionContainer
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.text.AnnotatedString
13 | import androidx.compose.ui.text.font.FontWeight
14 | import androidx.compose.ui.text.fromHtml
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.tooling.preview.Wallpapers
17 | import androidx.compose.ui.unit.dp
18 | import app.grapheneos.info.R
19 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
20 |
21 | @Composable
22 | fun CardanoScreen(
23 | modifier: Modifier = Modifier,
24 | showSnackbarError: (String) -> Unit = {},
25 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
26 | ) {
27 | ScreenLazyColumn(
28 | modifier = modifier
29 | .fillMaxSize(),
30 | additionalContentPadding = additionalContentPadding
31 | ) {
32 | item {
33 | Text(stringResource(R.string.cardano_info))
34 | }
35 | item {
36 | AddressInfoItem(
37 | title = "Cardano",
38 | qrCodePainterResourceId = R.drawable.donate_cardano_qr_code,
39 | qrCodeContentDescription = stringResource(R.string.cardano_qr_code_description),
40 | addressUrl = "web+cardano:addr1q9v89vfwyfssveug5zf2w7leafz8ethq490gvq0ghag883atfnucytpnq2t38dj7cnyngs6ne05cdwu9gseevgmt3ggq2a2wt6",
41 | address = "addr1q9v89vfwyfssveug5zf2w7leafz8ethq490gvq0ghag883atfnucytpnq2t38dj7cnyngs6ne05cdwu9gseevgmt3ggq2a2wt6",
42 | showSnackbarError = showSnackbarError
43 | )
44 | }
45 | item {
46 | SelectionContainer {
47 | Text(
48 | AnnotatedString.Companion.fromHtml(stringResource(R.string.cardano_handle_notice))
49 | )
50 | }
51 | }
52 | item {
53 | Text(
54 | stringResource(R.string.cardano_token_donation_notice),
55 | fontWeight = FontWeight.Bold
56 | )
57 | }
58 | }
59 | }
60 |
61 | @Preview(
62 | showBackground = true,
63 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
64 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
65 | )
66 | @Composable
67 | private fun CardanoScreenPreview() {
68 | MaterialTheme {
69 | CardanoScreen()
70 | }
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/grapheneos/tls/ModernTLSSocketFactory.java:
--------------------------------------------------------------------------------
1 | package org.grapheneos.tls;
2 |
3 | import java.io.IOException;
4 | import java.net.InetAddress;
5 | import java.net.Socket;
6 | import java.net.UnknownHostException;
7 | import java.security.KeyManagementException;
8 | import java.security.NoSuchAlgorithmException;
9 |
10 | import javax.net.ssl.SSLContext;
11 | import javax.net.ssl.SSLSocket;
12 | import javax.net.ssl.SSLSocketFactory;
13 |
14 | public class ModernTLSSocketFactory extends SSLSocketFactory {
15 | private final SSLSocketFactory wrapped;
16 |
17 | public ModernTLSSocketFactory() {
18 | try {
19 | final SSLContext context = SSLContext.getInstance("TLS");
20 | context.init(null, null, null);
21 | wrapped = context.getSocketFactory();
22 | } catch (final KeyManagementException | NoSuchAlgorithmException e) {
23 | throw new RuntimeException(e);
24 | }
25 | }
26 |
27 | @Override
28 | public String[] getDefaultCipherSuites() {
29 | return wrapped.getDefaultCipherSuites();
30 | }
31 |
32 | @Override
33 | public String[] getSupportedCipherSuites() {
34 | return wrapped.getSupportedCipherSuites();
35 | }
36 |
37 | @Override
38 | public Socket createSocket() throws IOException {
39 | return configureSocket(wrapped.createSocket());
40 | }
41 |
42 | @Override
43 | public Socket createSocket(final Socket s, final String host, final int port,
44 | final boolean autoClose) throws IOException {
45 | return configureSocket(wrapped.createSocket(s, host, port, autoClose));
46 | }
47 |
48 | @Override
49 | public Socket createSocket(final String host, final int port)
50 | throws IOException, UnknownHostException {
51 | return configureSocket(wrapped.createSocket(host, port));
52 | }
53 |
54 | @Override
55 | public Socket createSocket(final String host, final int port, final InetAddress localHost,
56 | final int localPort) throws IOException, UnknownHostException {
57 | return configureSocket(wrapped.createSocket(host, port, localHost, localPort));
58 | }
59 |
60 | @Override
61 | public Socket createSocket(final InetAddress host, final int port) throws IOException {
62 | return configureSocket(wrapped.createSocket(host, port));
63 | }
64 |
65 | @Override
66 | public Socket createSocket(final InetAddress address, final int port,
67 | final InetAddress localAddress, final int localPort) throws IOException {
68 | return configureSocket(wrapped.createSocket(address, port, localAddress, localPort));
69 | }
70 |
71 | private static Socket configureSocket(final Socket socket) {
72 | ((SSLSocket) socket).setEnabledProtocols(new String[] {"TLSv1.3"});
73 | return socket;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/preferences/PreferencesViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.preferences
2 |
3 | import android.app.Application
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.lifecycle.AndroidViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import app.grapheneos.info.InfoAppScreens
11 | import app.grapheneos.info.dataStore
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 | import kotlinx.coroutines.flow.collect
16 | import kotlinx.coroutines.flow.map
17 | import kotlinx.coroutines.flow.update
18 | import kotlinx.coroutines.launch
19 |
20 | class PreferencesViewModel(private val application: Application) : AndroidViewModel(application) {
21 | /**
22 | * Preferences state
23 | */
24 | private val _uiState = MutableStateFlow(PreferencesUiState())
25 | val uiState: StateFlow = _uiState.asStateFlow()
26 |
27 | init {
28 | viewModelScope.launch {
29 | // Populate the values of the preferences from the Preferences DataStore.
30 | application.dataStore.data.map { preferences ->
31 | fun getPreferencePair(preference: Pair, MutableState>): Pair, MutableState> {
32 | return Pair(
33 | preference.first,
34 | mutableStateOf(
35 | preferences[preference.first] ?: preference.second.value
36 | )
37 | )
38 | }
39 |
40 | _uiState.update {
41 | PreferencesUiState(
42 | isPreferencesLoaded = mutableStateOf(true),
43 | startDestination = getPreferencePair(uiState.value.startDestination).let {
44 | // migrate from old value
45 | if (it.second.value == "ReleaseNotes") {
46 | it.copy(
47 | second = mutableStateOf(InfoAppScreens.Releases.name)
48 | )
49 | } else {
50 | it
51 | }
52 | }
53 | )
54 | }
55 | }.collect()
56 | }
57 | }
58 |
59 | /**
60 | * Set a preference to a value and save to Preferences DataStore
61 | */
62 | fun setPreference(key: Preferences.Key, value: String) {
63 | viewModelScope.launch {
64 | application.dataStore.edit { preferences ->
65 | preferences[key] = value
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
13 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/reusablecomposables/ClickableText.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.reusablecomposables
2 |
3 | import androidx.compose.foundation.gestures.detectTapGestures
4 | import androidx.compose.foundation.text.InlineTextContent
5 | import androidx.compose.material3.LocalContentColor
6 | import androidx.compose.material3.LocalTextStyle
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.takeOrElse
14 | import androidx.compose.ui.input.pointer.pointerInput
15 | import androidx.compose.ui.text.AnnotatedString
16 | import androidx.compose.ui.text.TextLayoutResult
17 | import androidx.compose.ui.text.TextStyle
18 | import androidx.compose.ui.text.font.FontFamily
19 | import androidx.compose.ui.text.font.FontStyle
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.text.style.TextDecoration
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.TextUnit
25 |
26 | @Composable
27 | fun ClickableText(
28 | text: AnnotatedString,
29 | modifier: Modifier = Modifier,
30 | color: Color = Color.Unspecified,
31 | fontSize: TextUnit = TextUnit.Unspecified,
32 | fontStyle: FontStyle? = null,
33 | fontWeight: FontWeight? = null,
34 | fontFamily: FontFamily? = null,
35 | letterSpacing: TextUnit = TextUnit.Unspecified,
36 | textDecoration: TextDecoration? = null,
37 | textAlign: TextAlign? = null,
38 | lineHeight: TextUnit = TextUnit.Unspecified,
39 | overflow: TextOverflow = TextOverflow.Clip,
40 | softWrap: Boolean = true,
41 | maxLines: Int = Int.MAX_VALUE,
42 | minLines: Int = 1,
43 | inlineContent: Map = mapOf(),
44 | onTextLayout: (TextLayoutResult) -> Unit = {},
45 | style: TextStyle = LocalTextStyle.current,
46 | onClick: (Int) -> Unit,
47 | ) {
48 | val textColor = color.takeOrElse {
49 | style.color.takeOrElse {
50 | LocalContentColor.current
51 | }
52 | }
53 | val layoutResult = remember { mutableStateOf(null) }
54 | val pressIndicator = Modifier.pointerInput(onClick) {
55 | detectTapGestures { pos ->
56 | layoutResult.value?.let { layoutResult ->
57 | onClick(layoutResult.getOffsetForPosition(pos))
58 | }
59 | }
60 | }
61 |
62 | Text(
63 | text = text,
64 | modifier = modifier.then(pressIndicator),
65 | style = style.merge(
66 | color = textColor,
67 | fontSize = fontSize,
68 | fontWeight = fontWeight,
69 | textAlign = textAlign ?: TextAlign.Unspecified,
70 | lineHeight = lineHeight,
71 | fontFamily = fontFamily,
72 | textDecoration = textDecoration,
73 | fontStyle = fontStyle,
74 | letterSpacing = letterSpacing
75 | ),
76 | onTextLayout = {
77 | layoutResult.value = it
78 | onTextLayout(it)
79 | },
80 | overflow = overflow,
81 | softWrap = softWrap,
82 | maxLines = maxLines,
83 | minLines = minLines,
84 | inlineContent = inlineContent
85 | )
86 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/GithubSponsorsScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.platform.LocalUriHandler
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.text.LinkAnnotation
13 | import androidx.compose.ui.text.SpanStyle
14 | import androidx.compose.ui.text.buildAnnotatedString
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.tooling.preview.Wallpapers
18 | import androidx.compose.ui.unit.dp
19 | import app.grapheneos.info.R
20 | import app.grapheneos.info.ui.reusablecomposables.ClickableText
21 | import app.grapheneos.info.ui.reusablecomposables.LinkCardItem
22 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
23 |
24 | @Composable
25 | fun GithubSponsorsScreen(
26 | modifier: Modifier = Modifier,
27 | showSnackbarError: (String) -> Unit = {},
28 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
29 | ) {
30 | val githubSponsorsUrl = "https://github.com/sponsors/thestinger"
31 |
32 | ScreenLazyColumn(
33 | modifier = modifier
34 | .fillMaxSize(),
35 | additionalContentPadding = additionalContentPadding
36 | ) {
37 | item {
38 | val localUriHandler = LocalUriHandler.current
39 |
40 | val annotatedString = buildAnnotatedString {
41 | append(stringResource(R.string.github_sponsors_description_part_1))
42 |
43 | append(" ")
44 |
45 | pushLink(LinkAnnotation.Url(githubSponsorsUrl))
46 | pushStringAnnotation("URL", githubSponsorsUrl)
47 | pushStyle(SpanStyle(color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold))
48 |
49 | append(stringResource(R.string.github_sponsors))
50 |
51 | pop()
52 | pop()
53 | pop()
54 |
55 | append(stringResource(R.string.github_sponsors_description_part_2))
56 | }
57 |
58 | ClickableText(
59 | text = annotatedString,
60 | onClick = { offset ->
61 | annotatedString
62 | .getStringAnnotations("URL", offset, offset).firstOrNull()
63 | ?.let { annotation ->
64 | localUriHandler.openUri(annotation.item)
65 | }
66 | },
67 | )
68 | }
69 | item {
70 | LinkCardItem(
71 | painter = painterResource(id = R.drawable.github),
72 | title = stringResource(id = R.string.github_sponsors),
73 | link = githubSponsorsUrl,
74 | showSnackbarError = showSnackbarError
75 | )
76 | }
77 | }
78 | }
79 |
80 | @Preview(
81 | showBackground = true,
82 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
83 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
84 | )
85 | @Composable
86 | private fun GithubSponsorsScreenPreview() {
87 | MaterialTheme {
88 | GithubSponsorsScreen()
89 | }
90 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/reusablecomposables/ScreenLazyColumn.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.reusablecomposables
2 |
3 | import androidx.compose.foundation.gestures.FlingBehavior
4 | import androidx.compose.foundation.gestures.ScrollableDefaults
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.WindowInsets
8 | import androidx.compose.foundation.layout.asPaddingValues
9 | import androidx.compose.foundation.layout.calculateEndPadding
10 | import androidx.compose.foundation.layout.calculateStartPadding
11 | import androidx.compose.foundation.layout.displayCutout
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.foundation.lazy.LazyListScope
14 | import androidx.compose.foundation.lazy.LazyListState
15 | import androidx.compose.foundation.lazy.rememberLazyListState
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.platform.LocalLayoutDirection
20 | import androidx.compose.ui.unit.LayoutDirection
21 | import androidx.compose.ui.unit.dp
22 |
23 | @Composable
24 | fun ScreenLazyColumn(
25 | modifier: Modifier = Modifier,
26 | state: LazyListState = rememberLazyListState(),
27 | contentPadding: PaddingValues = PaddingValues(bottom = 16.dp, start = 16.dp, end = 16.dp),
28 | additionalContentPadding: PaddingValues = PaddingValues(all = 0.dp),
29 | reverseLayout: Boolean = false,
30 | verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(16.dp),
31 | horizontalAlignment: Alignment.Horizontal = Alignment.Start,
32 | flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
33 | userScrollEnabled: Boolean = true,
34 | layoutDirection: LayoutDirection = LocalLayoutDirection.current,
35 | content: LazyListScope.() -> Unit
36 | ) {
37 | val displayCutout = WindowInsets.displayCutout.asPaddingValues()
38 | val startPadding = displayCutout.calculateStartPadding(layoutDirection)
39 | val endPadding = displayCutout.calculateEndPadding(layoutDirection)
40 | val contentPadding = PaddingValues(
41 | start = contentPadding.calculateStartPadding(layoutDirection)
42 | + additionalContentPadding.calculateStartPadding(layoutDirection),
43 | top = contentPadding.calculateTopPadding()
44 | + additionalContentPadding.calculateTopPadding(),
45 | end = contentPadding.calculateEndPadding(layoutDirection)
46 | + additionalContentPadding.calculateEndPadding(layoutDirection),
47 | bottom = contentPadding.calculateBottomPadding()
48 | + additionalContentPadding.calculateBottomPadding()
49 | ).let {
50 | PaddingValues(
51 | start = startPadding.coerceAtLeast(it.calculateStartPadding(layoutDirection)),
52 | top = it.calculateTopPadding(),
53 | end = endPadding.coerceAtLeast(it.calculateEndPadding(layoutDirection)),
54 | bottom = it.calculateBottomPadding()
55 | )
56 | }
57 |
58 | LazyColumn(
59 | modifier = modifier,
60 | state = state,
61 | contentPadding = contentPadding,
62 | reverseLayout = reverseLayout,
63 | verticalArrangement = verticalArrangement,
64 | horizontalAlignment = horizontalAlignment,
65 | flingBehavior = flingBehavior,
66 | userScrollEnabled = userScrollEnabled,
67 | content = content,
68 | )
69 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/DonateCryptoCurrencyStartScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.res.painterResource
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.tooling.preview.Wallpapers
13 | import androidx.compose.ui.unit.dp
14 | import app.grapheneos.info.R
15 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
16 | import app.grapheneos.info.ui.reusablecomposables.ScreenNavCardItem
17 |
18 | @Composable
19 | fun DonateCryptoCurrencyStartScreen(
20 | modifier: Modifier = Modifier,
21 | onNavigateToBitcoinScreen: () -> Unit = {},
22 | onNavigateToMoneroScreen: () -> Unit = {},
23 | onNavigateToZcashScreen: () -> Unit = {},
24 | onNavigateToEthereumScreen: () -> Unit = {},
25 | onNavigateToCardanoScreen: () -> Unit = {},
26 | onNavigateToLitecoinScreen: () -> Unit = {},
27 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
28 | ) {
29 | ScreenLazyColumn(
30 | modifier = modifier
31 | .fillMaxSize(),
32 | additionalContentPadding = additionalContentPadding
33 | ) {
34 | item {
35 | ScreenNavCardItem(
36 | painter = painterResource(id = R.drawable.bitcoin),
37 | title = stringResource(id = R.string.bitcoin)
38 | ) {
39 | onNavigateToBitcoinScreen()
40 | }
41 | }
42 | item {
43 | ScreenNavCardItem(
44 | painter = painterResource(id = R.drawable.monero),
45 | title = stringResource(id = R.string.monero)
46 | ) {
47 | onNavigateToMoneroScreen()
48 | }
49 | }
50 | item {
51 | ScreenNavCardItem(
52 | painter = painterResource(id = R.drawable.zcash),
53 | title = stringResource(id = R.string.zcash)
54 | ) {
55 | onNavigateToZcashScreen()
56 | }
57 | }
58 | item {
59 | ScreenNavCardItem(
60 | painter = painterResource(id = R.drawable.ethereum),
61 | title = stringResource(id = R.string.ethereum)
62 | ) {
63 | onNavigateToEthereumScreen()
64 | }
65 | }
66 | item {
67 | ScreenNavCardItem(
68 | painter = painterResource(id = R.drawable.cardano),
69 | title = stringResource(id = R.string.cardano)
70 | ) {
71 | onNavigateToCardanoScreen()
72 | }
73 | }
74 | item {
75 | ScreenNavCardItem(
76 | painter = painterResource(id = R.drawable.litecoin),
77 | title = stringResource(id = R.string.litecoin)
78 | ) {
79 | onNavigateToLitecoinScreen()
80 | }
81 | }
82 | }
83 | }
84 |
85 | @Preview(
86 | showBackground = true,
87 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
88 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
89 | )
90 | @Composable
91 | private fun DonateCryptoCurrencyStartScreenPreview() {
92 | MaterialTheme {
93 | DonateCryptoCurrencyStartScreen()
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/PaypalScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.AttachMoney
9 | import androidx.compose.material.icons.filled.CurrencyPound
10 | import androidx.compose.material.icons.filled.Euro
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.tooling.preview.Wallpapers
18 | import androidx.compose.ui.unit.dp
19 | import app.grapheneos.info.R
20 | import app.grapheneos.info.ui.reusablecomposables.LinkCardItem
21 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
22 |
23 | @Composable
24 | fun PaypalScreen(
25 | modifier: Modifier = Modifier,
26 | showSnackbarError: (String) -> Unit = {},
27 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
28 | ) {
29 | ScreenLazyColumn(
30 | modifier = modifier
31 | .fillMaxSize(),
32 | additionalContentPadding = additionalContentPadding
33 | ) {
34 | item {
35 | Text(stringResource(R.string.paypal_info_description_part_1))
36 | }
37 | item {
38 | Text(stringResource(R.string.paypal_info_description_part_2))
39 | }
40 | item {
41 | Text(
42 | stringResource(R.string.donation_links),
43 | Modifier.padding(top = 16.dp),
44 | style = MaterialTheme.typography.titleLarge
45 | )
46 | }
47 | item {
48 | LinkCardItem(
49 | imageVector = Icons.Filled.AttachMoney,
50 | title = stringResource(R.string.canadian_dollar_cad),
51 | link = "https://www.paypal.com/donate/?hosted_button_id=T8KRPYKU5QVNE",
52 | showSnackbarError = showSnackbarError
53 | )
54 | }
55 | item {
56 | LinkCardItem(
57 | imageVector = Icons.Filled.AttachMoney,
58 | title = stringResource(R.string.united_states_dollar_usd),
59 | link = "https://www.paypal.com/donate/?hosted_button_id=2S2BP8V4E7PXU",
60 | showSnackbarError = showSnackbarError
61 | )
62 | }
63 | item {
64 | LinkCardItem(
65 | imageVector = Icons.Filled.Euro,
66 | title = stringResource(R.string.euro_eur),
67 | link = "https://www.paypal.com/donate/?hosted_button_id=5SNPWEDS53HW4",
68 | showSnackbarError = showSnackbarError
69 | )
70 | }
71 | item {
72 | LinkCardItem(
73 | imageVector = Icons.Filled.CurrencyPound,
74 | title = stringResource(R.string.british_pound_gbp),
75 | link = "https://www.paypal.com/donate/?hosted_button_id=N498QNB7NPKU8",
76 | showSnackbarError = showSnackbarError
77 | )
78 | }
79 | item {
80 | Text(stringResource(R.string.paypal_fee_description))
81 | }
82 | }
83 | }
84 |
85 | @Preview(
86 | showBackground = true,
87 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
88 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
89 | )
90 | @Composable
91 | private fun PaypalScreenPreview() {
92 | MaterialTheme {
93 | PaypalScreen()
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/reusablecomposables/CardItem.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.reusablecomposables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material3.ElevatedCard
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.LocalContentColor
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.painter.Painter
16 | import androidx.compose.ui.graphics.vector.ImageVector
17 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
18 | import androidx.compose.ui.platform.LocalUriHandler
19 | import androidx.compose.ui.res.painterResource
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.semantics.Role
22 | import androidx.compose.ui.unit.dp
23 | import app.grapheneos.info.R
24 |
25 | @Composable
26 | fun CardItem(
27 | modifier: Modifier = Modifier,
28 | painter: Painter,
29 | title: String,
30 | onClickLabel: String,
31 | onClick: () -> Unit,
32 | ) {
33 | ElevatedCard(modifier) {
34 | Row(
35 | Modifier
36 | .fillMaxWidth()
37 | .clickable(
38 | onClickLabel = onClickLabel,
39 | role = Role.Button,
40 | onClick = onClick,
41 | )
42 | .padding(16.dp),
43 | verticalAlignment = Alignment.CenterVertically,
44 | ) {
45 | Icon(
46 | painter = painter,
47 | contentDescription = null,
48 | modifier = Modifier.size(50.dp),
49 | tint = LocalContentColor.current,
50 | )
51 | Text(
52 | text = title,
53 | modifier = Modifier.padding(horizontal = 16.dp),
54 | )
55 | }
56 | }
57 | }
58 |
59 | @Composable
60 | fun LinkCardItem(
61 | modifier: Modifier = Modifier,
62 | painter: Painter,
63 | title: String,
64 | onClickLabel: String = stringResource(R.string.link_card_item_on_click_label),
65 | link: String,
66 | showSnackbarError: (String) -> Unit
67 | ) {
68 | val localUriHandler = LocalUriHandler.current
69 | val openUriIllegalArguementExceptionSnackbarError =
70 | stringResource(R.string.browser_link_illegal_argument_exception_snackbar_error)
71 |
72 | CardItem(
73 | modifier = modifier,
74 | painter = painter,
75 | title = title,
76 | onClickLabel = onClickLabel,
77 | ) {
78 | try {
79 | localUriHandler.openUri(link)
80 | } catch (e: IllegalArgumentException) {
81 | showSnackbarError(
82 | openUriIllegalArguementExceptionSnackbarError
83 | )
84 | }
85 | }
86 | }
87 |
88 | @Composable
89 | fun LinkCardItem(
90 | modifier: Modifier = Modifier,
91 | imageVector: ImageVector,
92 | title: String,
93 | link: String,
94 | showSnackbarError: (String) -> Unit
95 | ) {
96 | LinkCardItem(
97 | modifier = modifier,
98 | painter = rememberVectorPainter(imageVector),
99 | title = title,
100 | link = link,
101 | showSnackbarError = showSnackbarError
102 | )
103 | }
104 |
105 | @Composable
106 | fun ScreenNavCardItem(
107 | modifier: Modifier = Modifier,
108 | painter: Painter = painterResource(id = R.drawable.outline_newsmode),
109 | title: String,
110 | navigateTo: () -> Unit,
111 | ) {
112 | CardItem(
113 | modifier = modifier,
114 | painter = painter,
115 | title = title,
116 | onClickLabel = stringResource(R.string.screen_nav_card_item_on_click_label),
117 | onClick = navigateTo,
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream
2 | import java.util.Properties
3 |
4 | val keystorePropertiesFile = rootProject.file("keystore.properties")
5 | val useKeystoreProperties = keystorePropertiesFile.canRead()
6 | val keystoreProperties = Properties()
7 | if (useKeystoreProperties) {
8 | keystoreProperties.load(FileInputStream(keystorePropertiesFile))
9 | }
10 |
11 | plugins {
12 | id("com.android.application")
13 | id("org.jetbrains.kotlin.android")
14 | id("org.jetbrains.kotlin.plugin.compose") version "2.2.10"
15 | }
16 |
17 | java {
18 | toolchain {
19 | languageVersion.set(JavaLanguageVersion.of(17))
20 | }
21 | }
22 |
23 | android {
24 | if (useKeystoreProperties) {
25 | signingConfigs {
26 | create("release") {
27 | storeFile = rootProject.file(keystoreProperties["storeFile"]!!)
28 | storePassword = keystoreProperties["storePassword"] as String
29 | keyAlias = keystoreProperties["keyAlias"] as String
30 | keyPassword = keystoreProperties["keyPassword"] as String
31 | enableV4Signing = true
32 | }
33 | }
34 | }
35 |
36 | namespace = "app.grapheneos.info"
37 | compileSdk = 36
38 | buildToolsVersion = "36.1.0"
39 | ndkVersion = "29.0.14206865"
40 |
41 | defaultConfig {
42 | applicationId = "app.grapheneos.info"
43 | minSdk = 33
44 | targetSdk = 36
45 | versionCode = 7
46 | versionName = versionCode.toString()
47 |
48 | vectorDrawables {
49 | useSupportLibrary = true
50 | }
51 |
52 | ndk {
53 | abiFilters.clear()
54 | abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
55 | }
56 | }
57 |
58 | buildFeatures {
59 | compose = true
60 | buildConfig = true
61 | }
62 |
63 | androidResources {
64 | generateLocaleConfig = true
65 | localeFilters += listOf("en")
66 | }
67 |
68 | buildTypes {
69 | release {
70 | isMinifyEnabled = true
71 | isShrinkResources = true
72 | proguardFiles(
73 | getDefaultProguardFile("proguard-android-optimize.txt"),
74 | "proguard-rules.pro"
75 | )
76 | if (useKeystoreProperties) {
77 | signingConfig = signingConfigs.getByName("release")
78 | }
79 | }
80 | getByName("debug") {
81 | applicationIdSuffix = ".debug"
82 | versionNameSuffix = "-debug"
83 | signingConfig = signingConfigs.getByName("debug")
84 | }
85 | create("staging") {
86 | initWith(getByName("release"))
87 | applicationIdSuffix = ".debug"
88 | versionNameSuffix = "-debug"
89 | signingConfig = signingConfigs.getByName("debug")
90 | }
91 | }
92 | }
93 |
94 | dependencies {
95 | implementation("androidx.core:core-ktx:1.17.0")
96 | implementation("androidx.activity:activity-compose:1.10.1")
97 | implementation("androidx.navigation:navigation-compose:2.9.3")
98 | implementation("androidx.datastore:datastore-preferences:1.1.7")
99 | val lifecycleVersion = "2.9.3"
100 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
101 | implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
102 |
103 | implementation(platform("androidx.compose:compose-bom:2025.08.01"))
104 | implementation("androidx.compose.ui:ui")
105 | implementation("androidx.compose.ui:ui-text")
106 | implementation("androidx.compose.ui:ui-graphics")
107 | implementation("androidx.compose.ui:ui-tooling-preview")
108 | implementation("androidx.compose.material3:material3")
109 | implementation("androidx.compose.material3:material3-adaptive-navigation-suite")
110 | implementation("androidx.compose.material:material-icons-core")
111 | implementation("androidx.compose.material:material-icons-extended")
112 | debugImplementation("androidx.compose.ui:ui-tooling")
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/banktransfers/AccountInfoItem.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.banktransfers
2 |
3 | import android.content.ClipData
4 | import android.content.res.Configuration
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.text.selection.SelectionContainer
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.ContentCopy
13 | import androidx.compose.material3.ElevatedCard
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.rememberCoroutineScope
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.platform.ClipEntry
23 | import androidx.compose.ui.platform.LocalClipboard
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.tooling.preview.Wallpapers
27 | import androidx.compose.ui.unit.dp
28 | import app.grapheneos.info.R
29 | import kotlinx.coroutines.launch
30 |
31 | @Composable
32 | fun AccountInfoItemEntry(
33 | term: String,
34 | description: String,
35 | ) {
36 | val clipboard = LocalClipboard.current
37 | val clipboardScope = rememberCoroutineScope()
38 |
39 | Row(
40 | modifier = Modifier.fillMaxWidth(),
41 | horizontalArrangement = Arrangement.SpaceBetween,
42 | verticalAlignment = Alignment.CenterVertically,
43 | ) {
44 | Column(
45 | modifier = Modifier.weight(0.90f)
46 | ) {
47 | Text(
48 | text = term,
49 | style = MaterialTheme.typography.titleMedium
50 | )
51 | Text(
52 | text = description,
53 | )
54 | }
55 | IconButton(
56 | onClick = {
57 | clipboardScope.launch {
58 | clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(term, description)))
59 | }
60 | },
61 | modifier = Modifier.weight(0.10f)
62 | ) {
63 | Icon(
64 | imageVector = Icons.Filled.ContentCopy,
65 | contentDescription = stringResource(R.string.account_info_item_copy_term, term)
66 | )
67 | }
68 | }
69 | }
70 |
71 | @Composable
72 | fun AccountInfoItem(
73 | title: String,
74 | content: @Composable () -> Unit,
75 | ) {
76 | SelectionContainer {
77 | ElevatedCard(Modifier.fillMaxWidth()) {
78 | Column(
79 | Modifier
80 | .fillMaxWidth()
81 | .padding(16.dp),
82 | horizontalAlignment = Alignment.Start
83 | ) {
84 | Text(
85 | title,
86 | Modifier
87 | .padding(bottom = 24.dp),
88 | style = MaterialTheme.typography.titleLarge
89 | )
90 | Column(
91 | verticalArrangement = Arrangement.spacedBy(16.dp)
92 | ) {
93 | content()
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | @Preview(
101 | showBackground = true,
102 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
103 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
104 | )
105 | @Composable
106 | private fun AccountInfoItemPreview() {
107 | MaterialTheme {
108 | AccountInfoItem(
109 | title = "Title"
110 | ) {
111 | AccountInfoItemEntry(
112 | term = "Account holder",
113 | description = "Example"
114 | )
115 | AccountInfoItemEntry(
116 | term = "Special Numbers",
117 | description = "39438293483924"
118 | )
119 | AccountInfoItemEntry(
120 | term = "Etc",
121 | description = "another description"
122 | )
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/BitcoinScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.text.selection.SelectionContainer
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.text.SpanStyle
17 | import androidx.compose.ui.text.buildAnnotatedString
18 | import androidx.compose.ui.text.font.FontStyle
19 | import androidx.compose.ui.tooling.preview.Preview
20 | import androidx.compose.ui.tooling.preview.Wallpapers
21 | import androidx.compose.ui.unit.dp
22 | import app.grapheneos.info.R
23 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
24 |
25 | @Composable
26 | fun BitcoinScreen(
27 | modifier: Modifier = Modifier,
28 | showSnackbarError: (String) -> Unit = {},
29 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
30 | ) {
31 | ScreenLazyColumn(
32 | modifier = modifier
33 | .fillMaxSize(),
34 | additionalContentPadding = additionalContentPadding
35 | ) {
36 | item {
37 | Text(stringResource(R.string.bitcoin_info))
38 | }
39 | item {
40 | AddressInfoItem(
41 | title = "Bech32 (Segwit)",
42 | qrCodePainterResourceId = R.drawable.donate_bitcoin_qr_code,
43 | qrCodeContentDescription = stringResource(R.string.bech32_segwit_qr_code_description),
44 | addressUrl = "bitcoin:bc1q9qw3g8tdxf3dugkv2z8cahd3axehph0mhsqk96?label=GrapheneOS%20Foundation&message=Donation%20to%20GrapheneOS%20Foundation",
45 | address = "bc1q9qw3g8tdxf3dugkv2z8cahd3axehph0mhsqk96",
46 | showSnackbarError = showSnackbarError,
47 | )
48 | }
49 | item {
50 | AddressInfoItem(
51 | title = "Bech32m (Taproot)",
52 | qrCodePainterResourceId = R.drawable.donate_bitcoin_taproot_qr_code,
53 | qrCodeContentDescription = stringResource(R.string.bech32m_taproot_qr_code_description),
54 | addressUrl = "bitcoin:bc1prqf5hks5dnd4j87wxw3djn20559yhj7wvvcv6fqxpwlg96udkzgqtamhry?label=GrapheneOS%20Foundation&message=Donation%20to%20GrapheneOS%20Foundation",
55 | address = "bc1prqf5hks5dnd4j87wxw3djn20559yhj7wvvcv6fqxpwlg96udkzgqtamhry",
56 | showSnackbarError = showSnackbarError
57 | )
58 | }
59 | item {
60 | AddressInfoItem(
61 | title = "BIP47 payment code (stealth address)",
62 | qrCodePainterResourceId = R.drawable.donate_bitcoin_bip47_qr_code,
63 | qrCodeContentDescription = stringResource(R.string.bip47_payment_code_stealth_address_qr_code_description),
64 | addressUrl = "bitcoin:PM8TJKmhJNQX6UTFagyuBk8UGmwKM6yDovEokpHBscPgP3Ac7WdK5zaQKh5XLSawyxiGYZS2a7HkAoeL6oHg7Ahn1VXX888yRG4PwF1dojouPtW7tEHT",
65 | address = "PM8TJKmhJNQX6UTFagyuBk8UGmwKM6yDovEokpHBscPgP3Ac7WdK5zaQKh5XLSawyxiGYZS2a7HkAoeL6oHg7Ahn1VXX888yRG4PwF1dojouPtW7tEHT",
66 | showSnackbarError = showSnackbarError
67 | ) {
68 | Row(
69 | modifier = Modifier.fillMaxWidth(),
70 | verticalAlignment = Alignment.CenterVertically,
71 | horizontalArrangement = Arrangement.Start
72 | ) {
73 | SelectionContainer {
74 | Text(
75 | buildAnnotatedString {
76 | append("PayNym: ")
77 |
78 | pushStyle(SpanStyle(fontStyle = FontStyle.Italic))
79 |
80 | append("+GrapheneOS")
81 |
82 | pop()
83 | }
84 | )
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
92 | @Preview(
93 | showBackground = true,
94 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
95 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
96 | )
97 | @Composable
98 | private fun BitcoinScreenPreview() {
99 | MaterialTheme {
100 | BitcoinScreen()
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.releases
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyListState
10 | import androidx.compose.foundation.lazy.items
11 | import androidx.compose.material3.Button
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Text
14 | import androidx.compose.material3.pulltorefresh.PullToRefreshBox
15 | import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.DisposableEffect
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.rememberCoroutineScope
21 | import androidx.compose.runtime.saveable.rememberSaveable
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.platform.LocalUriHandler
25 | import androidx.compose.ui.res.stringResource
26 | import androidx.compose.ui.unit.dp
27 | import androidx.lifecycle.Lifecycle
28 | import androidx.lifecycle.LifecycleEventObserver
29 | import androidx.lifecycle.compose.LocalLifecycleOwner
30 | import app.grapheneos.info.R
31 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
32 | import kotlinx.coroutines.launch
33 |
34 | @OptIn(ExperimentalMaterial3Api::class)
35 | @Composable
36 | fun ReleasesScreen(
37 | modifier: Modifier = Modifier,
38 | showSnackbarError: (String) -> Unit,
39 | entries: List>,
40 | updateChangelog: (useCaches: Boolean, finishedUpdating: () -> Unit) -> Unit,
41 | changelogLazyListState: LazyListState,
42 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
43 | ) {
44 | val lifecycleOwner = LocalLifecycleOwner.current
45 |
46 | val localUriHandler = LocalUriHandler.current
47 |
48 | val refreshCoroutineScope = rememberCoroutineScope()
49 |
50 | val openUriIllegalArguementExceptionSnackbarError =
51 | stringResource(R.string.browser_link_illegal_argument_exception_snackbar_error)
52 |
53 | DisposableEffect(lifecycleOwner) {
54 | val observer = LifecycleEventObserver { _, event ->
55 | if (event == Lifecycle.Event.ON_START) {
56 | refreshCoroutineScope.launch {
57 | updateChangelog(true) {}
58 | }
59 | }
60 | }
61 |
62 | lifecycleOwner.lifecycle.addObserver(observer)
63 |
64 | onDispose {
65 | lifecycleOwner.lifecycle.removeObserver(observer)
66 | }
67 | }
68 |
69 | var isRefreshing by rememberSaveable { mutableStateOf(false) }
70 |
71 | val state = rememberPullToRefreshState()
72 |
73 | PullToRefreshBox(
74 | isRefreshing = isRefreshing,
75 | onRefresh = {
76 | isRefreshing = true
77 | updateChangelog(false) {
78 | isRefreshing = false
79 |
80 | refreshCoroutineScope.launch {
81 | state.animateToHidden()
82 | }
83 | }
84 | },
85 | state = state,
86 | modifier = modifier
87 | .fillMaxSize()
88 | ) {
89 | ScreenLazyColumn(
90 | modifier = Modifier
91 | .fillMaxSize(),
92 | state = changelogLazyListState,
93 | additionalContentPadding = additionalContentPadding,
94 | verticalArrangement = Arrangement.Top
95 | ) {
96 | items(
97 | items = entries,
98 | key = { it.first }) {
99 | Changelog(
100 | modifier = Modifier
101 | .fillMaxWidth()
102 | .padding(vertical = 16.dp),
103 | it.second
104 | )
105 | }
106 |
107 | item {
108 | Row(
109 | modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
110 | horizontalArrangement = Arrangement.Center,
111 | ) {
112 | Button(onClick = {
113 | try {
114 | localUriHandler.openUri("https://grapheneos.org/releases")
115 | } catch (_: IllegalArgumentException) {
116 | showSnackbarError(openUriIllegalArguementExceptionSnackbarError)
117 | }
118 | }) {
119 | Text(text = stringResource(R.string.releases_see_all_button))
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/community/CommunityScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.community
2 |
3 | import android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.MaterialTheme.typography
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.semantics.heading
15 | import androidx.compose.ui.semantics.semantics
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.tooling.preview.Wallpapers
18 | import androidx.compose.ui.unit.dp
19 | import app.grapheneos.info.R
20 | import app.grapheneos.info.ui.reusablecomposables.LinkCardItem
21 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
22 |
23 | @Composable
24 | fun CommunityScreen(
25 | modifier: Modifier = Modifier,
26 | showSnackbarError: (String) -> Unit = {},
27 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
28 | ) {
29 | ScreenLazyColumn(
30 | modifier = modifier
31 | .fillMaxSize(),
32 | additionalContentPadding = additionalContentPadding
33 | ) {
34 | item {
35 | Text(
36 | stringResource(R.string.chat),
37 | modifier = Modifier.semantics { heading() },
38 | style = typography.titleLarge
39 | )
40 | }
41 | item {
42 | Text(stringResource(R.string.community_screen_chat_rooms_description))
43 | }
44 | item {
45 | LinkCardItem(
46 | painter = painterResource(id = R.drawable.discord_logo),
47 | title = stringResource(R.string.discord),
48 | link = "https://discord.com/invite/grapheneos",
49 | showSnackbarError = showSnackbarError
50 | )
51 | }
52 | item {
53 | LinkCardItem(
54 | painter = painterResource(id = R.drawable.telegram_logo),
55 | title = stringResource(R.string.telegram),
56 | link = "https://t.me/GrapheneOS",
57 | showSnackbarError = showSnackbarError
58 | )
59 | }
60 | item {
61 | LinkCardItem(
62 | painter = painterResource(id = R.drawable.matrix_logo),
63 | title = stringResource(R.string.matrix),
64 | link = "https://matrix.to/#/%23community:grapheneos.org",
65 | showSnackbarError = showSnackbarError
66 | )
67 | }
68 | item {
69 | Text(
70 | stringResource(R.string.forum),
71 | modifier = Modifier
72 | .padding(top = 16.dp)
73 | .semantics { heading() },
74 | style = typography.titleLarge
75 | )
76 | }
77 | item {
78 | Text(stringResource(R.string.forum_description))
79 | }
80 | item {
81 | LinkCardItem(
82 | painter = painterResource(id = R.drawable.grapheneos_logo),
83 | title = stringResource(R.string.grapheneos_discussion_forum),
84 | link = "https://discuss.grapheneos.org",
85 | showSnackbarError = showSnackbarError
86 | )
87 | }
88 | item {
89 | Text(
90 | stringResource(R.string.social_media),
91 | modifier = Modifier
92 | .padding(top = 16.dp)
93 | .semantics { heading() },
94 | style = typography.titleLarge
95 | )
96 | }
97 | item {
98 | Text(
99 | stringResource(R.string.social_media_accounts_description)
100 | )
101 | }
102 | item {
103 | LinkCardItem(
104 | painter = painterResource(id = R.drawable.x_logo),
105 | title = stringResource(R.string.twitter),
106 | link = "https://x.com/GrapheneOS",
107 | showSnackbarError = showSnackbarError
108 | )
109 | }
110 | item {
111 | LinkCardItem(
112 | painter = painterResource(id = R.drawable.mastodon_logo),
113 | title = stringResource(R.string.mastodon),
114 | link = "https://grapheneos.social/@GrapheneOS",
115 | showSnackbarError = showSnackbarError
116 | )
117 | }
118 | item {
119 | LinkCardItem(
120 | painter = painterResource(id = R.drawable.bluesky_logo),
121 | title = stringResource(R.string.bluesky),
122 | link = "https://bsky.app/profile/grapheneos.org",
123 | showSnackbarError = showSnackbarError
124 | )
125 | }
126 | }
127 | }
128 |
129 | @Preview(
130 | showBackground = true,
131 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
132 | uiMode = UI_MODE_NIGHT_UNDEFINED
133 | )
134 | @Composable
135 | private fun CommunityScreenPreview() {
136 | MaterialTheme {
137 | CommunityScreen()
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/DonateStartScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalUriHandler
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.semantics.heading
18 | import androidx.compose.ui.semantics.semantics
19 | import androidx.compose.ui.text.LinkAnnotation
20 | import androidx.compose.ui.text.SpanStyle
21 | import androidx.compose.ui.text.buildAnnotatedString
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.tooling.preview.Preview
24 | import androidx.compose.ui.tooling.preview.Wallpapers
25 | import androidx.compose.ui.unit.dp
26 | import app.grapheneos.info.R
27 | import app.grapheneos.info.ui.reusablecomposables.ClickableText
28 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
29 | import app.grapheneos.info.ui.reusablecomposables.ScreenNavCardItem
30 |
31 | @Composable
32 | fun DonateStartScreen(
33 | modifier: Modifier = Modifier,
34 | onNavigateToGithubSponsorsScreen: () -> Unit = {},
35 | onNavigateToCryptocurrenciesScreen: () -> Unit = {},
36 | onNavigateToPayPalScreen: () -> Unit = {},
37 | onNavigateToBankTransfersScreen: () -> Unit = {},
38 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
39 | ) {
40 | ScreenLazyColumn(
41 | modifier = modifier
42 | .fillMaxSize(),
43 | additionalContentPadding = additionalContentPadding
44 | ) {
45 | item {
46 | Text(stringResource(R.string.donate_start_info_part_1))
47 | }
48 | item {
49 | Text(stringResource(R.string.donate_start_info_part_2))
50 | }
51 | item {
52 | Text(
53 | stringResource(R.string.donate_the_way_you_want),
54 | modifier = Modifier.padding(top = 16.dp)
55 | .semantics { heading() },
56 | style = MaterialTheme.typography.titleLarge
57 | )
58 | }
59 | item {
60 | Text(stringResource(R.string.donate_start_info_part_3))
61 | }
62 | item {
63 | ScreenNavCardItem(
64 | painter = painterResource(id = R.drawable.github),
65 | title = stringResource(id = R.string.github_sponsors)
66 | ) {
67 | onNavigateToGithubSponsorsScreen()
68 | }
69 | }
70 | item {
71 | ScreenNavCardItem(
72 | painter = painterResource(id = R.drawable.crypto),
73 | title = stringResource(id = R.string.cryptocurrencies)
74 | ) {
75 | onNavigateToCryptocurrenciesScreen()
76 | }
77 | }
78 | item {
79 | ScreenNavCardItem(
80 | painter = painterResource(id = R.drawable.paypal),
81 | title = stringResource(id = R.string.paypal)
82 | ) {
83 | onNavigateToPayPalScreen()
84 | }
85 | }
86 | item {
87 | ScreenNavCardItem(
88 | painter = painterResource(id = R.drawable.bank),
89 | title = stringResource(id = R.string.bank_transfers)
90 | ) {
91 | onNavigateToBankTransfersScreen()
92 | }
93 | }
94 | item {
95 | val localUriHandler = LocalUriHandler.current
96 | val annotatedString = buildAnnotatedString {
97 | append(stringResource(R.string.hiring_footer_part_1))
98 |
99 | val hiringUrl = "https://grapheneos.org/hiring"
100 | pushLink(LinkAnnotation.Url(hiringUrl))
101 | pushStringAnnotation("URL", hiringUrl)
102 | pushStyle(SpanStyle(color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold))
103 |
104 | append(stringResource(R.string.hiring_footer_part_2))
105 |
106 | pop()
107 | pop()
108 | pop()
109 |
110 | append(stringResource(R.string.hiring_footer_part_3))
111 | }
112 |
113 | Row(
114 | modifier = Modifier
115 | .fillMaxWidth()
116 | .padding(bottom = 16.dp),
117 | horizontalArrangement = Arrangement.Center
118 | ) {
119 | ClickableText(
120 | text = annotatedString,
121 | onClick = { offset ->
122 | annotatedString
123 | .getStringAnnotations("URL", offset, offset).firstOrNull()
124 | ?.let { annotation ->
125 | localUriHandler.openUri(annotation.item)
126 | }
127 | },
128 | )
129 | }
130 | }
131 | }
132 | }
133 |
134 | @Preview(
135 | showBackground = true,
136 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
137 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
138 | )
139 | @Composable
140 | private fun DonateScreenPreview() {
141 | MaterialTheme {
142 | DonateStartScreen()
143 | }
144 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.releases
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.lifecycle.AndroidViewModel
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.viewModelScope
8 | import app.grapheneos.info.R
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.withContext
15 | import org.grapheneos.tls.ModernTLSSocketFactory
16 | import java.io.IOException
17 | import java.net.SocketTimeoutException
18 | import java.net.URL
19 | import java.net.UnknownServiceException
20 | import javax.net.ssl.HttpsURLConnection
21 |
22 | const val TAG = "ReleasesViewModel"
23 |
24 | class ReleasesViewModel(
25 | private val application: Application,
26 | savedStateHandle: SavedStateHandle
27 | ) : AndroidViewModel(application) {
28 |
29 | private val tlsSocketFactory = ModernTLSSocketFactory()
30 | private val _uiState = MutableStateFlow(ReleasesUiState(savedStateHandle))
31 | val uiState: StateFlow = _uiState.asStateFlow()
32 |
33 | init {
34 | updateChangelog(
35 | useCaches = true,
36 | showSnackbarError = {},
37 | scrollChangelogLazyListTo = {},
38 | countAsInitialScroll = false,
39 | onFinishedUpdating = {},
40 | )
41 | }
42 |
43 | fun updateChangelog(
44 | useCaches: Boolean,
45 | showSnackbarError: suspend (message: String) -> Unit,
46 | scrollChangelogLazyListTo: (scrollTo: Int) -> Unit,
47 | countAsInitialScroll: Boolean = true,
48 | onFinishedUpdating: () -> Unit = {},
49 | ) {
50 | viewModelScope.launch(Dispatchers.IO) {
51 | try {
52 | val url = URL("https://grapheneos.org/releases.atom")
53 | val connection = url.openConnection() as HttpsURLConnection
54 |
55 | connection.apply {
56 | sslSocketFactory = tlsSocketFactory
57 | connectTimeout = 10_000
58 | readTimeout = 30_000
59 | }
60 |
61 | try {
62 | connection.useCaches = useCaches
63 |
64 | connection.connect()
65 |
66 | val responseText = String(connection.inputStream.readBytes())
67 |
68 | var newEntries = "(.*?)".toRegex().findAll(responseText).map { it.groups[1]!!.value }.map { entry ->
69 | Pair("(.*?)".toRegex().find(entry)?.groups?.get(1)?.value ?: entry.hashCode().toString(), entry)
70 | }.toMap()
71 |
72 | var currentOsChangelogIndex = newEntries.toSortedMap().toList().asReversed().indexOfFirst { entry ->
73 | val title = "(.*?)".toRegex()
74 | .find(entry.second)?.groups?.get(1)?.value
75 |
76 | title == android.os.Build.VERSION.INCREMENTAL
77 | }
78 |
79 | if (currentOsChangelogIndex == -1) {
80 | currentOsChangelogIndex = 0
81 | }
82 |
83 | newEntries = newEntries.toSortedMap().toList().asReversed().filterIndexed { index, _ ->
84 | index <= currentOsChangelogIndex + 3
85 | }.toMap()
86 |
87 | // Only update if there are changes to the number of changelogs
88 | if ((newEntries.count() - uiState.value.entries.size) != 0) {
89 | withContext(Dispatchers.Main) {
90 | _uiState.value.entries.filterKeys {
91 | !newEntries.keys.contains(it)
92 | }.forEach {
93 | _uiState.value.entries.remove(it.key)
94 | }
95 | _uiState.value.entries.putAll(newEntries)
96 | }
97 | }
98 |
99 | if (countAsInitialScroll && !uiState.value.didInitialScroll) {
100 | _uiState.value.didInitialScroll = true
101 | scrollChangelogLazyListTo(currentOsChangelogIndex)
102 | }
103 | } catch (e: SocketTimeoutException) {
104 | val errorMessage =
105 | application.getString(R.string.update_changelog_socket_timeout_exception_snackbar_message)
106 | Log.e(TAG, errorMessage, e)
107 | viewModelScope.launch {
108 | showSnackbarError("$errorMessage: $e")
109 | }
110 | } catch (e: IOException) {
111 | val errorMessage =
112 | application.getString(R.string.update_changelog_io_exception_snackbar_message)
113 | Log.e(TAG, errorMessage, e)
114 | viewModelScope.launch {
115 | showSnackbarError("$errorMessage: $e")
116 | }
117 | } catch (e: UnknownServiceException) {
118 | val errorMessage =
119 | application.getString(R.string.update_changelog_unknown_service_exception_snackbar_message)
120 | Log.e(TAG, errorMessage, e)
121 | viewModelScope.launch {
122 | showSnackbarError("$errorMessage: $e")
123 | }
124 | } finally {
125 | connection.disconnect()
126 | }
127 | } catch (e: IOException) {
128 | val errorMessage =
129 | application.getString(R.string.update_changelog_failed_to_create_httpsurlconnection_snackbar_message)
130 | Log.e(TAG, errorMessage, e)
131 | viewModelScope.launch {
132 | showSnackbarError("$errorMessage: $e")
133 | }
134 | } finally {
135 | onFinishedUpdating()
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/cryptocurrency/AddressInfoItem.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.cryptocurrency
2 |
3 | import android.content.Intent
4 | import androidx.activity.compose.LocalActivity
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.aspectRatio
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.filled.Share
16 | import androidx.compose.material3.AlertDialog
17 | import androidx.compose.material3.ElevatedCard
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.IconButton
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Text
22 | import androidx.compose.material3.TextButton
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.saveable.rememberSaveable
27 | import androidx.compose.runtime.setValue
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.draw.clip
31 | import androidx.compose.ui.platform.LocalUriHandler
32 | import androidx.compose.ui.res.painterResource
33 | import androidx.compose.ui.res.stringResource
34 | import androidx.compose.ui.semantics.Role
35 | import androidx.compose.ui.text.LinkAnnotation
36 | import androidx.compose.ui.text.SpanStyle
37 | import androidx.compose.ui.text.buildAnnotatedString
38 | import androidx.compose.ui.text.font.FontWeight
39 | import androidx.compose.ui.unit.dp
40 | import androidx.core.app.ActivityOptionsCompat
41 | import app.grapheneos.info.R
42 | import app.grapheneos.info.ui.reusablecomposables.ClickableText
43 |
44 | @Composable
45 | fun AddressInfoItem(
46 | title: String,
47 | qrCodePainterResourceId: Int,
48 | qrCodeContentDescription: String,
49 | addressUrl: String,
50 | address: String,
51 | showSnackbarError: (String) -> Unit,
52 | bottomContent: @Composable () -> Unit = {},
53 | ) {
54 | val localUriHandler = LocalUriHandler.current
55 |
56 | var showAlertDialog by rememberSaveable { mutableStateOf(false) }
57 |
58 | val activity = LocalActivity.current
59 |
60 | val activityNotFoundForDonationAddressSnackbarErrorMessage = stringResource(R.string.activity_not_found_for_donation_address_snackbar_error)
61 |
62 | ElevatedCard(Modifier.fillMaxWidth()) {
63 | Column(
64 | Modifier
65 | .fillMaxWidth()
66 | .padding(16.dp),
67 | horizontalAlignment = Alignment.CenterHorizontally
68 | ) {
69 | Text(
70 | title,
71 | Modifier
72 | .padding(bottom = 24.dp)
73 | .align(Alignment.Start),
74 | style = MaterialTheme.typography.titleLarge
75 | )
76 | Box(
77 | modifier = Modifier
78 | .padding(bottom = 16.dp)
79 | .clip(RoundedCornerShape(20.dp))
80 | .background(MaterialTheme.colorScheme.primaryContainer)
81 | ) {
82 | Icon(
83 | painter = painterResource(id = qrCodePainterResourceId),
84 | contentDescription = qrCodeContentDescription,
85 | modifier = Modifier
86 | .size(200.dp)
87 | .align(Alignment.Center)
88 | .clickable(
89 | onClickLabel = stringResource(R.string.qr_code_on_click_label),
90 | role = Role.Image
91 | ) {
92 | showAlertDialog = true
93 | },
94 | tint = MaterialTheme.colorScheme.onPrimaryContainer
95 | )
96 | }
97 |
98 | val annotatedString = buildAnnotatedString {
99 | pushLink(LinkAnnotation.Url(addressUrl))
100 | pushStringAnnotation("URL", addressUrl)
101 | pushStyle(SpanStyle(color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold))
102 |
103 | append(address)
104 |
105 | pop()
106 | pop()
107 | pop()
108 | }
109 |
110 | ClickableText(
111 | text = annotatedString,
112 | onClick = { offset ->
113 | annotatedString
114 | .getStringAnnotations("URL", offset, offset).firstOrNull()
115 | ?.let { annotation ->
116 | try {
117 | localUriHandler.openUri(annotation.item)
118 | } catch (e: IllegalArgumentException) {
119 | showSnackbarError(activityNotFoundForDonationAddressSnackbarErrorMessage)
120 | }
121 | }
122 | },
123 | )
124 | IconButton(
125 | onClick = {
126 | var sendIntent = Intent()
127 |
128 | sendIntent = sendIntent.apply {
129 | action = Intent.ACTION_SEND
130 | putExtra(Intent.EXTRA_TEXT, address)
131 | type = "text/plain"
132 | }
133 |
134 | val shareIntent = Intent.createChooser(sendIntent, null)
135 | activity?.startActivity(
136 | shareIntent,
137 | ActivityOptionsCompat.makeBasic().toBundle()
138 | )
139 | }
140 | ) {
141 | Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
142 | }
143 | bottomContent()
144 | }
145 | if (showAlertDialog) {
146 | AlertDialog(
147 | onDismissRequest = { showAlertDialog = false },
148 | confirmButton = {
149 | TextButton(
150 | onClick = { showAlertDialog = false }
151 | ) {
152 | Text(stringResource(R.string.ok))
153 | }
154 | },
155 | text = {
156 | Box(
157 | modifier = Modifier
158 | .clip(RoundedCornerShape(20.dp))
159 | .background(MaterialTheme.colorScheme.primaryContainer)
160 | ) {
161 | Icon(
162 | painter = painterResource(id = qrCodePainterResourceId),
163 | contentDescription = stringResource(R.string.image_enlarged, qrCodeContentDescription),
164 | modifier = Modifier
165 | .aspectRatio(1.0f)
166 | .align(Alignment.Center),
167 | tint = MaterialTheme.colorScheme.onPrimaryContainer
168 | )
169 | }
170 | }
171 | )
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/donate_ethereum_qr_code.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 | -
9 |
10 |
11 | -
12 |
13 |
14 | -
15 |
16 |
17 | -
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 |
26 |
27 |
28 |
29 | -
30 |
31 |
32 |
33 |
34 |
35 | -
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 |
45 |
46 | -
47 |
48 |
49 |
50 |
51 | -
52 |
53 |
54 |
55 |
56 | -
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 | -
67 |
68 |
69 |
70 |
71 | -
72 |
73 |
74 | -
75 |
76 |
77 |
78 |
79 | -
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 |
89 | -
90 |
91 |
92 |
93 |
94 | -
95 |
96 |
97 |
98 |
99 | -
100 |
101 |
102 | -
103 |
104 |
105 | -
106 |
107 |
108 | -
109 |
110 |
111 | -
112 |
113 |
114 |
115 |
116 | -
117 |
118 |
119 | -
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Info
3 | Releases
4 | Community
5 | Donate
6 | GitHub Sponsors
7 | Cryptocurrencies
8 | PayPal
9 | Bank Transfers
10 | Bitcoin
11 | Monero
12 | Zcash
13 | Ethereum
14 | Cardano
15 | Litecoin
16 | Local Bank Transfer to Wise
17 | EU/SEPA (EUR)
18 | Account holder
19 | GrapheneOS Foundation
20 | IBAN
21 | Bank name
22 | Wise and Bank address
23 | UK (GBP)
24 | Account number
25 | Sort code
26 | US (USD)
27 | Routing number
28 | Account type
29 | Checking
30 | Wise address
31 | Bank address
32 | Australia (AUD)
33 | BSB code
34 | New Zealand (NZD)
35 | Canada (CAD)
36 | Transit number
37 | Institution number
38 | Hungary (HUF)
39 | Turkey (TRY)
40 | Interac e-Transfer
41 | If you have a Canadian bank account, you can send Canadian dollar
42 | donations to the non-profit GrapheneOS Foundation via Interac e-Transfer to <i>contact@grapheneos.org</i>. The email address has Interac e-Transfer Autodeposit support enabled
43 | so no security question is necessary. If your bank doesn\'t support Autodeposit, set the answer to the security
44 | question to GrapheneOS.
45 |
46 | Navigate up
47 | GrapheneOS is an open source project supported via donations from
48 | individuals, companies and other organizations.
49 |
50 | Donations are used for paying developers, purchasing hardware
51 | (workstations, test devices, debugging cables/boards, etc.), paying for infrastructure (domains,
52 | virtual/dedicated servers) and paying legal fees.
53 |
54 | Donate the way you want
55 | GrapheneOS offers the following ways to donate.\nChoose the one you
56 | prefer.
57 |
58 | "Contribute in other ways?\nCheck out our "
59 | hiring page
60 | .
61 | Chat
62 | Our chat rooms are bridged across Discord, Telegram and Matrix so
63 | you can choose your preferred platform.
64 |
65 | Discord
66 | Telegram
67 | Matrix
68 | Forum
69 | We have an official forum for longer form posts, which is publicly accessible and
70 | easier to search.
71 |
72 | GrapheneOS Discussion Forum
73 | Social Media
74 | The project\'s official social media accounts are used for official
75 | announcements.
76 |
77 | X
78 | Mastodon
79 | Bluesky
80 | GrapheneOS can be sponsored with recurring or one-time donations
81 | via credit cards through
82 |
83 | . There are standard tiers from $5 to $5,000 or you can donate a
84 | custom amount.
85 |
86 | PayPal can be used to make one-time, monthly or yearly donations to the
87 | non-profit GrapheneOS Foundation.
88 |
89 | If possible, use the donation link for your currency. If it\'s not listed,
90 | please use the CAD donation link.
91 |
92 | Donation links
93 | Canadian Dollar (CAD)
94 | United States dollar (USD)
95 | Euro (EUR)
96 | British pound (GBP)
97 | PayPal charges a base fee of 30 cents and 2.9% of the donation amount within
98 | Canada. There\'s an additional 0.8% fee for donations from the US and 1% for other countries. Currency
99 | conversion adds an additional 4% fee as opposed to the usual PayPal conversion fee of 3%.
100 |
101 | open link
102 | Bitcoin can be used to make donations to the non-profit GrapheneOS Foundation.
103 | Bitcoin donation QR code
104 | Bitcoin Taproot donation QR code
105 | Bitcoin BIP47 payment code QR code
106 | enlarge QR code
107 | Share
108 | OK
109 | %1$s (enlarged)
110 | Couldn\'t find an app to open donation address
111 | with!
112 |
113 | Cardano can be used to make donations to the non-profit GrapheneOS Foundation.
114 | Cardano donation QR code
115 |
116 | We own the <i>$grapheneos</i> handle with this address so you can also send to the handle.
117 | We aren\'t looking for donations of tokens, only Cardano itself.
118 |
119 | Ethereum can be used to make donations to the non-profit GrapheneOS Foundation.
120 |
121 | Ethereum donation QR code
122 | We aren\'t looking for donations of tokens, only Ethereum itself.
123 |
124 | Litecoin can be used to make donations to the non-profit GrapheneOS Foundation.
125 |
126 | Litecoin donation QR code
127 | Monero can be used to make donations to the non-profit GrapheneOS Foundation.
128 | Monero donation QR code
129 | Zcash can be used to make donations to the non-profit GrapheneOS Foundation.
130 | Transparent Zcash donation QR code
131 | Socket Timeout Exception
132 | Failed to retrieve latest release notes
133 | Unknown Service Exception
134 | Failed to create
135 | HttpsURLConnection
136 |
137 | open screen
138 | Copy %1$s
139 | Unable to open link. Make sure a browser is installed and enabled on your device.
140 | Info about the releases
141 | See all release notes
142 |
143 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 |
118 |
119 | # Determine the Java command to use to start the JVM.
120 | if [ -n "$JAVA_HOME" ] ; then
121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
122 | # IBM's JDK on AIX uses strange locations for the executables
123 | JAVACMD=$JAVA_HOME/jre/sh/java
124 | else
125 | JAVACMD=$JAVA_HOME/bin/java
126 | fi
127 | if [ ! -x "$JAVACMD" ] ; then
128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
129 |
130 | Please set the JAVA_HOME variable in your environment to match the
131 | location of your Java installation."
132 | fi
133 | else
134 | JAVACMD=java
135 | if ! command -v java >/dev/null 2>&1
136 | then
137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
138 |
139 | Please set the JAVA_HOME variable in your environment to match the
140 | location of your Java installation."
141 | fi
142 | fi
143 |
144 | # Increase the maximum file descriptors if we can.
145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
146 | case $MAX_FD in #(
147 | max*)
148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
149 | # shellcheck disable=SC2039,SC3045
150 | MAX_FD=$( ulimit -H -n ) ||
151 | warn "Could not query maximum file descriptor limit"
152 | esac
153 | case $MAX_FD in #(
154 | '' | soft) :;; #(
155 | *)
156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
157 | # shellcheck disable=SC2039,SC3045
158 | ulimit -n "$MAX_FD" ||
159 | warn "Could not set maximum file descriptor limit to $MAX_FD"
160 | esac
161 | fi
162 |
163 | # Collect all arguments for the java command, stacking in reverse order:
164 | # * args from the command line
165 | # * the main class name
166 | # * -classpath
167 | # * -D...appname settings
168 | # * --module-path (only if needed)
169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
170 |
171 | # For Cygwin or MSYS, switch paths to Windows format before running java
172 | if "$cygwin" || "$msys" ; then
173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/donate/banktransfers/BankTransfersScreen.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.donate.banktransfers
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.text.selection.SelectionContainer
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.text.AnnotatedString
14 | import androidx.compose.ui.text.fromHtml
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.tooling.preview.Wallpapers
17 | import androidx.compose.ui.unit.dp
18 | import app.grapheneos.info.R
19 | import app.grapheneos.info.ui.reusablecomposables.ScreenLazyColumn
20 |
21 | @Composable
22 | fun BankTransfersScreen(
23 | modifier: Modifier = Modifier,
24 | additionalContentPadding: PaddingValues = PaddingValues(0.dp)
25 | ) {
26 | ScreenLazyColumn(
27 | modifier = modifier
28 | .fillMaxSize(),
29 | additionalContentPadding = additionalContentPadding
30 | ) {
31 | item {
32 | Text(
33 | stringResource(R.string.local_bank_transfer_to_wise),
34 | modifier = Modifier.padding(top = 16.dp),
35 | style = MaterialTheme.typography.titleLarge
36 | )
37 | }
38 | item {
39 | AccountInfoItem(
40 | title = stringResource(R.string.eu_sepa_eur)
41 | ) {
42 | AccountInfoItemEntry(
43 | term = stringResource(R.string.account_holder),
44 | description = stringResource(R.string.grapheneos_foundation)
45 | )
46 | AccountInfoItemEntry(
47 | term = "IBAN",
48 | description = "BE20 9677 1140 7056"
49 | )
50 | AccountInfoItemEntry(
51 | term = "BIC",
52 | description = "TRWIBEB1XXX"
53 | )
54 | AccountInfoItemEntry(
55 | term = stringResource(R.string.bank_name),
56 | description = "Wise Europe SA"
57 | )
58 | AccountInfoItemEntry(
59 | term = stringResource(R.string.wise_and_bank_address),
60 | description = "Rue du Trône 100, 3rd floor\n" +
61 | "Brussels\n" +
62 | "1050\n" +
63 | "Belgium"
64 | )
65 | }
66 | }
67 | item {
68 | AccountInfoItem(
69 | title = stringResource(R.string.uk_gbp)
70 | ) {
71 | AccountInfoItemEntry(
72 | term = stringResource(R.string.account_holder),
73 | description = stringResource(R.string.grapheneos_foundation)
74 | )
75 | AccountInfoItemEntry(
76 | term = stringResource(R.string.account_number),
77 | description = "49883070"
78 | )
79 | AccountInfoItemEntry(
80 | term = "BIC",
81 | description = "TRWIBEB1XXX"
82 | )
83 | AccountInfoItemEntry(
84 | term = "IBAN",
85 | description = "GB68 TRWI 2314 7049 8830 70"
86 | )
87 | AccountInfoItemEntry(
88 | term = stringResource(R.string.sort_code),
89 | description = "23-14-70"
90 | )
91 | AccountInfoItemEntry(
92 | term = stringResource(R.string.bank_name),
93 | description = "Wise Payments Limited"
94 | )
95 | AccountInfoItemEntry(
96 | term = stringResource(R.string.wise_and_bank_address),
97 | description = "56 Shoreditch High Street\n" +
98 | "London\n" +
99 | "E1 6JJ\n" +
100 | "United Kingdom"
101 | )
102 | }
103 | }
104 | item {
105 | AccountInfoItem(
106 | title = stringResource(R.string.us_usd)
107 | ) {
108 | AccountInfoItemEntry(
109 | term = stringResource(R.string.account_holder),
110 | description = stringResource(R.string.grapheneos_foundation)
111 | )
112 | AccountInfoItemEntry(
113 | term = stringResource(R.string.account_number),
114 | description = "8313560023"
115 | )
116 | AccountInfoItemEntry(
117 | term = stringResource(R.string.routing_number),
118 | description = "026073150"
119 | )
120 | AccountInfoItemEntry(
121 | term = stringResource(R.string.account_type),
122 | description = stringResource(R.string.checking)
123 | )
124 | AccountInfoItemEntry(
125 | term = stringResource(R.string.wise_address),
126 | description = "30 W. 26th Street, Sixth Floor\n" +
127 | "New York NY\n" +
128 | "10010\n" +
129 | "United States"
130 | )
131 | AccountInfoItemEntry(
132 | term = stringResource(R.string.bank_name),
133 | description = "Community Federal Savings Bank"
134 | )
135 | AccountInfoItemEntry(
136 | term = stringResource(R.string.bank_address),
137 | description = "89-16 Jamaica Ave\n" +
138 | "Woodhaven NY\n" +
139 | "11421\n" +
140 | "United States"
141 | )
142 | }
143 | }
144 | item {
145 | AccountInfoItem(
146 | title = stringResource(R.string.australia_aud)
147 | ) {
148 | AccountInfoItemEntry(
149 | term = stringResource(R.string.account_holder),
150 | description = stringResource(R.string.grapheneos_foundation)
151 | )
152 | AccountInfoItemEntry(
153 | term = stringResource(R.string.account_number),
154 | description = "213524417"
155 | )
156 | AccountInfoItemEntry(
157 | term = stringResource(R.string.bsb_code),
158 | description = "774-001"
159 | )
160 | AccountInfoItemEntry(
161 | term = stringResource(R.string.bank_name),
162 | description = "Wise Australia Pty Ltd"
163 | )
164 | AccountInfoItemEntry(
165 | term = stringResource(R.string.wise_address),
166 | description = "Suite 1, Level 11, 66 Goulburn Street\n" +
167 | "Sydney\n" +
168 | "2000\n" +
169 | "Australia"
170 | )
171 | }
172 | }
173 | item {
174 | AccountInfoItem(
175 | title = stringResource(R.string.new_zealand_nzd)
176 | ) {
177 | AccountInfoItemEntry(
178 | term = stringResource(R.string.account_holder),
179 | description = stringResource(R.string.grapheneos_foundation)
180 | )
181 | AccountInfoItemEntry(
182 | term = stringResource(R.string.account_number),
183 | description = "04-2021-0151878-36"
184 | )
185 | AccountInfoItemEntry(
186 | term = stringResource(R.string.wise_address),
187 | description = "56 Shoreditch High Street\n" +
188 | "London\n" +
189 | "E1 6JJ\n" +
190 | "United Kingdom"
191 | )
192 | AccountInfoItemEntry(
193 | term = stringResource(R.string.bank_name),
194 | description = "JPMorgan Chase"
195 | )
196 | AccountInfoItemEntry(
197 | term = stringResource(R.string.bank_address),
198 | description = "Head Office, Pwc Tower\n" +
199 | "Auckland\n" +
200 | "1010\n" +
201 | "New Zealand"
202 | )
203 | }
204 | }
205 | item {
206 | AccountInfoItem(
207 | title = stringResource(R.string.canada_cad)
208 | ) {
209 | AccountInfoItemEntry(
210 | term = stringResource(R.string.account_holder),
211 | description = stringResource(R.string.grapheneos_foundation)
212 | )
213 | AccountInfoItemEntry(
214 | term = stringResource(R.string.account_number),
215 | description = "200110745303"
216 | )
217 | AccountInfoItemEntry(
218 | term = stringResource(R.string.transit_number),
219 | description = "16001"
220 | )
221 | AccountInfoItemEntry(
222 | term = stringResource(R.string.institution_number),
223 | description = "621"
224 | )
225 | AccountInfoItemEntry(
226 | term = stringResource(R.string.wise_address),
227 | description = "99 Bank Street, Suite 1420\n" +
228 | "Ottawa ON\n" +
229 | "K1P 1H4\n" +
230 | "Canada"
231 | )
232 | AccountInfoItemEntry(
233 | term = stringResource(R.string.bank_name),
234 | description = "Peoples Trust"
235 | )
236 | AccountInfoItemEntry(
237 | term = stringResource(R.string.bank_address),
238 | description = "595 Burrard Street\n" +
239 | "Vancouver BC\n" +
240 | "V7X 1L7\n" +
241 | "Canada"
242 | )
243 | }
244 | }
245 | item {
246 | AccountInfoItem(
247 | title = stringResource(R.string.hungary_huf)
248 | ) {
249 | AccountInfoItemEntry(
250 | term = stringResource(R.string.account_holder),
251 | description = stringResource(R.string.grapheneos_foundation)
252 | )
253 | AccountInfoItemEntry(
254 | term = stringResource(R.string.account_number),
255 | description = "12600016-11020392-99827322"
256 | )
257 | AccountInfoItemEntry(
258 | term = stringResource(R.string.bank_name),
259 | description = "Wise Europe SA"
260 | )
261 | AccountInfoItemEntry(
262 | term = stringResource(R.string.wise_and_bank_address),
263 | description = "Rue du Trône 100, 3rd floor\n" +
264 | "Brussels\n" +
265 | "1050\n" +
266 | "Belgium"
267 | )
268 | }
269 | }
270 | item {
271 | AccountInfoItem(
272 | title = stringResource(R.string.turkey_try)
273 | ) {
274 | AccountInfoItemEntry(
275 | term = stringResource(R.string.account_holder),
276 | description = stringResource(R.string.grapheneos_foundation)
277 | )
278 | AccountInfoItemEntry(
279 | term = stringResource(R.string.iban),
280 | description = "TR43 0010 3000 0000 0057 4294 70"
281 | )
282 | AccountInfoItemEntry(
283 | term = stringResource(R.string.wise_address),
284 | description = "56 Shoreditch High Street, London, E1 6JJ, United Kingdom"
285 | )
286 | AccountInfoItemEntry(
287 | term = stringResource(R.string.bank_name),
288 | description = "Fibabanka A.Ş."
289 | )
290 | AccountInfoItemEntry(
291 | term = stringResource(R.string.bank_address),
292 | description = "Büyükdere Cad. 129, Esentepe Mah., Sisli"
293 | )
294 | }
295 | }
296 | item {
297 | Text(
298 | stringResource(R.string.interac_e_transfer),
299 | modifier = Modifier.padding(top = 16.dp),
300 | style = MaterialTheme.typography.titleLarge
301 | )
302 | }
303 | item {
304 | SelectionContainer {
305 | Text(AnnotatedString.Companion.fromHtml(stringResource(R.string.interac_e_transfer_info)))
306 | }
307 | }
308 | }
309 | }
310 |
311 | @Preview(
312 | showBackground = true,
313 | wallpaper = Wallpapers.RED_DOMINATED_EXAMPLE,
314 | uiMode = Configuration.UI_MODE_NIGHT_UNDEFINED
315 | )
316 | @Composable
317 | private fun BankTransfersScreenPreview() {
318 | MaterialTheme {
319 | BankTransfersScreen()
320 | }
321 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/grapheneos/info/ui/releases/Changelog.kt:
--------------------------------------------------------------------------------
1 | package app.grapheneos.info.ui.releases
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.text.selection.SelectionContainer
7 | import androidx.compose.material3.ElevatedCard
8 | import androidx.compose.material3.LocalTextStyle
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.MaterialTheme.typography
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalUriHandler
15 | import androidx.compose.ui.platform.UriHandler
16 | import androidx.compose.ui.semantics.heading
17 | import androidx.compose.ui.semantics.semantics
18 | import androidx.compose.ui.text.AnnotatedString
19 | import androidx.compose.ui.text.LinkAnnotation
20 | import androidx.compose.ui.text.SpanStyle
21 | import androidx.compose.ui.text.TextStyle
22 | import androidx.compose.ui.text.font.FontStyle
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.withStyle
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import app.grapheneos.info.ui.reusablecomposables.ClickableText
28 | import org.w3c.dom.Document
29 | import org.w3c.dom.Node
30 | import org.xml.sax.InputSource
31 | import java.io.StringReader
32 | import javax.xml.parsers.DocumentBuilderFactory
33 |
34 | @Composable
35 | fun Changelog(modifier: Modifier = Modifier, entry: String) {
36 | val localUriHandler = LocalUriHandler.current
37 |
38 | SelectionContainer {
39 | ElevatedCard(modifier) {
40 | Column(Modifier.padding(16.dp)) {
41 | val factory = DocumentBuilderFactory.newInstance()
42 | val builder = factory.newDocumentBuilder()
43 |
44 | val document: Document = builder.parse(InputSource(StringReader("$entry")))
45 |
46 | document.documentElement.normalize()
47 |
48 | NodeToComposable(
49 | node = document.documentElement,
50 | modifier = Modifier,
51 | style = LocalTextStyle.current,
52 | builder = AnnotatedString.Builder(),
53 | localUriHandler = localUriHandler
54 | )
55 | }
56 | }
57 | }
58 | }
59 |
60 | @Composable
61 | private fun NodeToComposable(
62 | node: Node,
63 | modifier: Modifier,
64 | style: TextStyle,
65 | builder: AnnotatedString.Builder,
66 | localUriHandler: UriHandler
67 | ) {
68 | val attributes = node.attributes
69 |
70 | // Push annotations and modify modifier and/or style
71 | for (a in 0 until attributes.length) {
72 | val attribute = attributes.item(a)
73 |
74 | when (attribute.nodeName) {
75 | "href" -> {
76 | val hrefValue = attribute.nodeValue
77 | val home = "https://grapheneos.org"
78 | val url = if (hrefValue.startsWith('/')) {
79 | "$home$hrefValue"
80 | } else if (hrefValue.startsWith('#')) {
81 | "$home/releases$hrefValue"
82 | } else {
83 | hrefValue
84 | }
85 | builder.apply {
86 | pushLink(LinkAnnotation.Url(url))
87 | pushStringAnnotation("URL", url)
88 | pushStyle(SpanStyle(color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold))
89 | }
90 | }
91 |
92 | "aria-label" -> {
93 | // Only VerbatimTtsAnnotation is available so we can't use the text contained for TTS
94 | }
95 |
96 | else -> {
97 | }
98 | }
99 | }
100 |
101 | ParseChildren(node, modifier, style, builder, localUriHandler)
102 |
103 | // Pop annotations, modifier and style don't carry over so no need to do anything for those
104 | for (a in 0 until attributes.length) {
105 | val attribute = attributes.item(a)
106 |
107 | when (attribute.nodeName) {
108 | "href" -> {
109 | builder.apply {
110 | pop()
111 | pop()
112 | pop()
113 | }
114 | }
115 |
116 | "aria-label" -> {
117 | }
118 |
119 | else -> {
120 | }
121 | }
122 | }
123 | }
124 |
125 | @Composable
126 | private fun ParseChildren(
127 | node: Node,
128 | modifier: Modifier,
129 | style: TextStyle,
130 | builder: AnnotatedString.Builder,
131 | localUriHandler: UriHandler
132 | ) {
133 | val children = node.childNodes
134 |
135 | for (i in 0 until children.length) {
136 | val child = children.item(i)
137 |
138 | when (child.nodeType) {
139 | Node.ELEMENT_NODE -> {
140 | when (child.nodeName) {
141 | "title" -> {
142 | val annotatedStringBuilder = AnnotatedString.Builder()
143 |
144 | NodeToComposable(
145 | child,
146 | modifier,
147 | style,
148 | annotatedStringBuilder,
149 | localUriHandler
150 | )
151 |
152 | val annotatedString = annotatedStringBuilder.toAnnotatedString()
153 |
154 | ClickableText(
155 | text = annotatedString,
156 | modifier = modifier.semantics { heading() },
157 | onClick = { offset ->
158 | annotatedString
159 | .getStringAnnotations("URL", offset, offset).firstOrNull()
160 | ?.let { annotation ->
161 | localUriHandler.openUri(annotation.item)
162 | }
163 | },
164 | style = typography.titleLarge,
165 | )
166 | }
167 |
168 | "content" -> NodeToComposable(
169 | child,
170 | modifier,
171 | style,
172 | AnnotatedString.Builder(),
173 | localUriHandler
174 | )
175 |
176 | "div" -> NodeToComposable(
177 | child,
178 | modifier,
179 | style,
180 | AnnotatedString.Builder(),
181 | localUriHandler
182 | )
183 |
184 | "p" -> {
185 | val annotatedStringBuilder = AnnotatedString.Builder()
186 |
187 | NodeToComposable(
188 | child,
189 | modifier,
190 | style,
191 | annotatedStringBuilder,
192 | localUriHandler,
193 | )
194 |
195 | val annotatedString = annotatedStringBuilder.toAnnotatedString()
196 |
197 | val likelyHeading =
198 | (annotatedString.text == "Tags:") || (annotatedString.startsWith("Changes since the"))
199 |
200 | ClickableText(
201 | text = annotatedString,
202 | onClick = { offset ->
203 | annotatedString
204 | .getStringAnnotations("URL", offset, offset).firstOrNull()
205 | ?.let { annotation ->
206 | localUriHandler.openUri(annotation.item)
207 | }
208 | },
209 | modifier = if (likelyHeading) {
210 | modifier.padding(top = 16.dp, bottom = 12.dp)
211 | } else {
212 | modifier.padding(top = 24.dp)
213 | },
214 | style = if (likelyHeading) {
215 | typography.titleMedium
216 | } else {
217 | style
218 | },
219 | )
220 | }
221 |
222 | "a" -> {
223 | NodeToComposable(
224 | child,
225 | modifier,
226 | style,
227 | builder,
228 | localUriHandler
229 | )
230 | }
231 |
232 | "ul" -> {
233 | NodeToComposable(
234 | child,
235 | modifier,
236 | style,
237 | AnnotatedString.Builder(),
238 | localUriHandler
239 | )
240 | }
241 |
242 | "li" -> {
243 | val annotatedStringBuilder = AnnotatedString.Builder()
244 |
245 | NodeToComposable(
246 | child,
247 | modifier,
248 | style,
249 | annotatedStringBuilder,
250 | localUriHandler
251 | )
252 |
253 | val annotatedString = annotatedStringBuilder.toAnnotatedString()
254 |
255 | Row(modifier = Modifier.padding(vertical = 2.dp)) {
256 | Text(
257 | when (child.parentNode.nodeName) {
258 | "ul" -> " • "
259 | "ol" -> " $i "
260 | else -> ""
261 | }
262 | )
263 |
264 | ClickableText(
265 | text = annotatedString,
266 | onClick = { offset ->
267 | annotatedString
268 | .getStringAnnotations("URL", offset, offset).firstOrNull()
269 | ?.let { annotation ->
270 | localUriHandler.openUri(annotation.item)
271 | }
272 | }
273 | )
274 | }
275 | }
276 |
277 | "b", "strong" -> {
278 | builder.withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
279 | NodeToComposable(
280 | child,
281 | modifier,
282 | style.copy(fontWeight = FontWeight.Bold),
283 | builder,
284 | localUriHandler,
285 | )
286 | }
287 | }
288 |
289 | "i", "em" -> {
290 | builder.withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
291 | NodeToComposable(
292 | child,
293 | modifier,
294 | style.copy(fontStyle = FontStyle.Italic),
295 | builder,
296 | localUriHandler
297 | )
298 | }
299 | }
300 |
301 | "span" -> {
302 | NodeToComposable(
303 | child,
304 | modifier,
305 | style,
306 | builder,
307 | localUriHandler
308 | )
309 | }
310 |
311 | "h1", "h2", "h3", "h4", "h5", "h6" -> {
312 | val annotatedStringBuilder = AnnotatedString.Builder()
313 |
314 | NodeToComposable(
315 | child,
316 | modifier,
317 | style,
318 | annotatedStringBuilder,
319 | localUriHandler,
320 | )
321 |
322 | val annotatedString = annotatedStringBuilder.toAnnotatedString()
323 |
324 | val likelyHeading =
325 | (annotatedString.text == "Tags:") || (annotatedString.startsWith("Changes since the"))
326 |
327 | ClickableText(
328 | text = annotatedString,
329 | onClick = { offset ->
330 | annotatedString
331 | .getStringAnnotations("URL", offset, offset).firstOrNull()
332 | ?.let { annotation ->
333 | localUriHandler.openUri(annotation.item)
334 | }
335 | },
336 | modifier = if (likelyHeading) {
337 | modifier.padding(top = 16.dp, bottom = 12.dp)
338 | } else {
339 | modifier.padding(vertical = 16.dp)
340 | },
341 | style = if (likelyHeading) {
342 | typography.titleMedium
343 | } else {
344 | when (child.nodeName) {
345 | "h1" -> style.copy(
346 | fontSize = 32.sp,
347 | fontWeight = FontWeight.Bold,
348 | )
349 |
350 | "h2" -> style.copy(
351 | fontSize = 24.sp,
352 | fontWeight = FontWeight.Bold,
353 | )
354 |
355 | "h3" -> style.copy(
356 | fontSize = 18.72.sp,
357 | fontWeight = FontWeight.Bold,
358 | )
359 |
360 | "h4" -> style.copy(
361 | fontSize = 16.sp,
362 | fontWeight = FontWeight.Bold,
363 | )
364 |
365 | "h5" -> style.copy(
366 | fontSize = 13.28.sp,
367 | fontWeight = FontWeight.Bold,
368 | )
369 |
370 | "h6" -> style.copy(
371 | fontSize = 10.72.sp,
372 | fontWeight = FontWeight.Bold,
373 | )
374 |
375 | else -> style
376 | }
377 | },
378 | )
379 | }
380 |
381 | else -> {
382 | NodeToComposable(
383 | child,
384 | modifier,
385 | style,
386 | builder,
387 | localUriHandler
388 | )
389 | }
390 | }
391 | }
392 |
393 | Node.TEXT_NODE -> {
394 | val textContent = child.textContent
395 | if (!textContent.isNullOrEmpty()) {
396 | builder.apply {
397 | append(textContent)
398 | }
399 | }
400 | }
401 | }
402 | }
403 | }
404 |
--------------------------------------------------------------------------------