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 -dontobfuscate
-dontoptimize -dontoptimize
# You can control the set of applied configuration files using the # 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 # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html

View file

@ -1,18 +1,37 @@
package de.jrpie.android.launcher.ui.settings.meta package de.jrpie.android.launcher.ui.settings.meta
import android.app.AlertDialog import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import androidx.compose.foundation.clickable
import android.widget.TextView 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 androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.openTutorial
@ -30,13 +49,21 @@ import de.jrpie.android.launcher.ui.UIObject
*/ */
class SettingsFragmentMeta : Fragment(), UIObject { class SettingsFragmentMeta : Fragment(), UIObject {
private lateinit var binding: SettingsMetaBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = SettingsMetaBinding.inflate(inflater, container, false) return ComposeView(requireContext()).apply {
return binding.root setContent {
MaterialTheme {
SettingsMetaScreen(
context = requireContext(),
onResetConfirmed = { requireActivity().finish() }
)
}
}
}
} }
override fun onStart() { override fun onStart() {
@ -45,99 +72,180 @@ class SettingsFragmentMeta : Fragment(), UIObject {
} }
override fun setOnClicks() { 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()
)
}
}
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()
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
// view code
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
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()
)
}
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()
)
}
}
}
// 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))
}
// version
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
binding.settingsMetaTextVersion.setOnClickListener {
val deviceInfo = getDeviceInfo()
copyToClipboard(requireContext(), deviceInfo)
}
} }
} }
// 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
)
}
}
}
// 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))
}
)
actions.forEachIndexed { index, action ->
SettingsButton(
text = stringResource(action.textResId),
onClick = { action.onClick(context) }
)
if (index == 1 || index == 3 || index == 6) {
SettingsButtonSpacer()
}
}
// 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()
copyToClipboard(context, deviceInfo)
}
)
Spacer(modifier = Modifier.height(48.dp))
}
}
// 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))
}
// 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))
}
},
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 # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true 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 for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.nonTransitiveRClass=true 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")