This commit is contained in:
Luke Wass 2025-05-13 03:40:50 +00:00 committed by GitHub
commit 5fa39f033d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 421 additions and 258 deletions

View file

@ -1,116 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
android {
dataBinding {
enabled = true
}
packaging {
resources.excludes.addAll(
[
"META-INF/LICENSE.md",
"META-INF/NOTICE.md",
"META-INF/LICENSE-notice.md"
]
)
}
defaultConfig {
applicationId "de.jrpie.android.launcher"
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
versionCode 46
versionName "0.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
}
flavorDimensions += "distribution"
productFlavors {
create("default") {
dimension = "distribution"
getIsDefault().set(true)
buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "true"
}
create("accrescent") {
dimension = "distribution"
applicationIdSuffix = ".accrescent"
versionNameSuffix = "+accrescent"
buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "false"
}
}
sourceSets {
accrescent {
manifest.srcFile 'src/accrescent/AndroidManifest.xml'
}
}
namespace 'de.jrpie.android.launcher'
buildFeatures {
buildConfig true
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
lint {
abortOnError false
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.activity:activity-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.palette:palette-ktx:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.12.0'
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation "eu.jonahbauer:android-preference-annotations:1.1.2"
implementation 'androidx.activity:activity:1.10.1'
annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2"
annotationProcessor "com.android.databinding:compiler:$android_plugin_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

109
app/build.gradle.kts Normal file
View file

@ -0,0 +1,109 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "de.jrpie.android.launcher"
compileSdk = 35
defaultConfig {
applicationId = "de.jrpie.android.launcher"
minSdk = 21
targetSdk = 35
versionCode = 46
versionName = "0.2.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
}
val distributionDimension = "distribution"
flavorDimensions += distributionDimension
productFlavors {
create("default") {
dimension = distributionDimension
isDefault = true
buildConfigField("boolean", "USE_ACCESSIBILITY_SERVICE", "true")
}
create("accrescent") {
dimension = distributionDimension
applicationIdSuffix = ".accrescent"
versionNameSuffix = "+accrescent"
buildConfigField("boolean", "USE_ACCESSIBILITY_SERVICE", "false")
}
}
sourceSets {
this.getByName("accrescent") {
this.java.srcDir("src/accrescent")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
compose = true
dataBinding = true
viewBinding = true
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
lint {
abortOnError = false
}
}
dependencies {
implementation(libs.androidx.activity)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.gridlayout)
implementation(libs.androidx.palette.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.recyclerview)
implementation(libs.google.material)
implementation(libs.kotlinx.serialization.json)
implementation(libs.jonahbauer.android.preference.annotations)
implementation(libs.androidx.ui.tooling.preview.android)
kapt(libs.jonahbauer.android.preference.annotations)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.test.ext.junit)
}

View file

@ -2,7 +2,7 @@
-dontobfuscate
-dontoptimize
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View file

@ -1,18 +1,37 @@
package de.jrpie.android.launcher.ui.settings.meta
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial
@ -30,13 +49,21 @@ import de.jrpie.android.launcher.ui.UIObject
*/
class SettingsFragmentMeta : Fragment(), UIObject {
private lateinit var binding: SettingsMetaBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = SettingsMetaBinding.inflate(inflater, container, false)
return binding.root
return ComposeView(requireContext()).apply {
setContent {
MaterialTheme {
SettingsMetaScreen(
context = requireContext(),
onResetConfirmed = { requireActivity().finish() }
)
}
}
}
}
override fun onStart() {
@ -45,99 +72,180 @@ class SettingsFragmentMeta : Fragment(), UIObject {
}
override fun setOnClicks() {
// No longer needed as click handlers are defined in Compose
}
}
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
// Data class to represent a settings action
private data class SettingsAction(
val textResId: Int,
val onClick: (Context) -> Unit
)
// Composable for the settings meta screen
@Composable
fun SettingsMetaScreen(
context: Context,
onResetConfirmed: () -> Unit
) {
val openAlertDialog = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
SettingsButtonList(context, openAlertDialog)
if (openAlertDialog.value) {
AlertDialogResetSettings(
onDismissRequest = { openAlertDialog.value = false },
onConfirmation = {
openAlertDialog.value = false
resetPreferences(context)
onResetConfirmed()
},
dialogTitle = stringResource(R.string.settings_meta_reset),
dialogText = stringResource(R.string.settings_meta_reset_confirm),
icon = Icons.Default.Warning
)
}
}
binding.settingsMetaButtonViewTutorial.setOnClickListener {
openTutorial(requireContext())
}
// prompting for settings-reset confirmation
binding.settingsMetaButtonResetSettings.setOnClickListener {
AlertDialog.Builder(this.requireContext(), R.style.AlertDialogCustom)
.setTitle(getString(R.string.settings_meta_reset))
.setMessage(getString(R.string.settings_meta_reset_confirm))
.setPositiveButton(
android.R.string.ok
) { _, _ ->
resetPreferences(this.requireContext())
requireActivity().finish()
// Composable for the scrollable button list and version number
@Composable
private fun SettingsButtonList(
context: Context,
openAlertDialog: MutableState<Boolean>
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val actions = listOf(
SettingsAction(R.string.settings_meta_show_tutorial) { openTutorial(it) },
SettingsAction(R.string.settings_meta_reset) { openAlertDialog.value = true },
SettingsAction(R.string.settings_meta_view_code) {
openInBrowser(it.getString(R.string.settings_meta_link_github), it)
},
SettingsAction(R.string.settings_meta_report_bug) {
openInBrowser(it.getString(R.string.settings_meta_report_bug_link), it)
},
SettingsAction(R.string.settings_meta_join_chat) {
openInBrowser(it.getString(R.string.settings_meta_chat_url), it)
},
SettingsAction(R.string.settings_meta_fork_contact) {
openInBrowser(it.getString(R.string.settings_meta_fork_contact_url), it)
},
SettingsAction(R.string.settings_meta_donate) {
openInBrowser(it.getString(R.string.settings_meta_donate_url), it)
},
SettingsAction(R.string.settings_meta_privacy) {
openInBrowser(it.getString(R.string.settings_meta_privacy_url), it)
},
SettingsAction(R.string.settings_meta_licenses) {
it.startActivity(Intent(it, LegalInfoActivity::class.java))
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
)
actions.forEachIndexed { index, action ->
SettingsButton(
text = stringResource(action.textResId),
onClick = { action.onClick(context) }
)
if (index == 1 || index == 3 || index == 6) {
SettingsButtonSpacer()
}
// view code
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
}
// Version number at the bottom of buttons
Text(
text = BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
color = colorResource(R.color.finnmglasTheme_text_color),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, end = 8.dp)
.clickable {
val deviceInfo = getDeviceInfo()
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setView(R.layout.dialog_report_bug)
setTitle(R.string.dialog_report_bug_title)
setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ ->
openInBrowser(
getString(R.string.settings_meta_report_bug_link),
requireContext()
copyToClipboard(context, deviceInfo)
}
)
Spacer(modifier = Modifier.height(48.dp))
}
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply {
val info = findViewById<TextView>(R.id.dialog_report_bug_device_info)
val buttonClipboard = findViewById<Button>(R.id.dialog_report_bug_button_clipboard)
val buttonSecurity = findViewById<Button>(R.id.dialog_report_bug_button_security)
info.text = deviceInfo
buttonClipboard.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
info.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
buttonSecurity.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_vulnerability_link),
requireContext()
// Composable for a settings button
@Composable
fun SettingsButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(8.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = colorResource(R.color.finnmglasTheme_accent_color),
contentColor = colorResource(R.color.finnmglasTheme_text_color)
)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorResource(R.color.finnmglasTheme_text_color)
)
}
}
// Composable for spacing between buttons
@Composable
fun SettingsButtonSpacer() {
Spacer(modifier = Modifier.height(16.dp))
}
// join chat
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
// contact developer
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
// contact fork developer
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
// donate
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
// privacy policy
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
// legal info
binding.settingsMetaButtonLicenses.setOnClickListener {
startActivity(Intent(this.context, LegalInfoActivity::class.java))
// Composable for the reset settings alert dialog
@Composable
fun AlertDialogResetSettings(
onDismissRequest: () -> Unit,
onConfirmation: () -> Unit,
dialogTitle: String,
dialogText: String,
icon: ImageVector
) {
AlertDialog(
icon = {
Icon(
imageVector = icon,
contentDescription = stringResource(android.R.string.dialog_alert_title),
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(text = dialogTitle)
},
text = {
Text(text = dialogText)
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onConfirmation) {
Text(stringResource(android.R.string.ok))
}
// version
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
binding.settingsMetaTextVersion.setOnClickListener {
val deviceInfo = getDeviceInfo()
copyToClipboard(requireContext(), deviceInfo)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(android.R.string.cancel))
}
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
)
}

View file

@ -1,35 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.0.0'
ext.android_plugin_version = '8.9.2'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.android.tools.build:gradle:$android_plugin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register('clean', Delete) {
delete layout.buildDirectory
}

13
build.gradle.kts Normal file
View file

@ -0,0 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.kapt) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
tasks.register("clean", Delete::class) {
delete(layout.buildDirectory)
}

View file

@ -15,8 +15,6 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.nonTransitiveRClass=true

64
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,64 @@
[versions]
activity = "1.10.1"
activity-ktx = "1.10.1"
activityCompose = "1.10.1"
agp = "8.10.0"
android-preference-annotations = "1.1.2"
appcompat = "1.7.0"
composeBom = "2025.05.00"
compose-latest = "1.8.1"
constraintlayout = "2.2.1"
core-ktx = "1.16.0"
espresso-core = "3.6.1"
gridlayout = "1.1.0"
junit = "4.13.2"
junitVersion = "1.2.1"
kapt = "1.8.10"
kotlin = "2.1.0"
kotlinx-serialization-json = "1.7.3"
material = "1.12.0"
palette-ktx = "1.0.0"
preference-ktx = "1.2.1"
recyclerview = "1.4.0"
serialization = "1.8.10"
uiToolingPreviewAndroid = "1.8.1"
[libraries]
# AndroidX dependencies
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "palette-ktx" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
# Google Material
google-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
# Kotlin and Serialization
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
# Annotation Processors
jonahbauer-android-preference-annotations = { group = "eu.jonahbauer", name = "android-preference-annotations", version.ref = "android-preference-annotations" }
# Testing dependencies
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kapt" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" }

View file

@ -1,2 +0,0 @@
rootProject.name='Launcher'
include ':app'

24
settings.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Launcher"
include(":app")