Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 104 additions & 48 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,41 @@
name: Android Release
name: Build Android APK

on:
push:
branches: [ "main" ]
workflow_dispatch: {}

permissions:
contents: write # notwendig, um Releases/Tags anzulegen
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-release:
build:
name: Build APK
runs-on: ubuntu-latest

env:
# Pfade an dein Modul anpassen, falls nicht "app"
APP_MODULE: app

steps:
- name: Checkout
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup JDK 17
uses: actions/setup-java@v4
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: "17"
cache: gradle

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Decode signing keystore
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
shell: bash
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > keystore.jks
echo "ANDROID_KEYSTORE_PATH=$GITHUB_WORKSPACE/keystore.jks" >> $GITHUB_ENV
distribution: 'temurin'
java-version: '17'

- name: Make gradlew executable
run: chmod +x gradlew
- name: Setup Gradle cache
uses: gradle/actions/setup-gradle@v5

# ---- Version aus Gradle lesen ----
- name: Read versionName from Gradle
id: ver
run: |
VERSION_NAME=$(./gradlew -q :${{ env.APP_MODULE }}:printVersionName)
VERSION_NAME=$(./gradlew -q :app:printVersionName)
echo "version_name=$VERSION_NAME" >> $GITHUB_OUTPUT

# ---- Release Build (APK & AAB) ----
- name: Build Release APK
run: ./gradlew :${{ env.APP_MODULE }}:assembleRelease
- name: Build debug APK
run: ./gradlew assembleDebug

- name: Build Release AAB
run: ./gradlew :${{ env.APP_MODULE }}:bundleRelease
- name: Build debug AAB
run: ./gradlew bundleDebug

# ---- Artefakte umbenennen ----
- name: Rename outputs to the correct version
Expand All @@ -63,14 +44,14 @@ jobs:
set -e
VER="${{ steps.ver.outputs.version_name }}"

APK_SRC="${{ env.APP_MODULE }}/build/outputs/apk/release"
AAB_SRC="${{ env.APP_MODULE }}/build/outputs/bundle/release"
APK_SRC="app/build/outputs/apk/debug"
AAB_SRC="app/build/outputs/bundle/debug"

APK_PATH=$(ls "$APK_SRC"/*.apk | head -n1)
AAB_PATH=$(ls "$AAB_SRC"/*.aab | head -n1)

APK_OUT="ipv64net_v${VER}.apk"
AAB_OUT="ipv64net_v${VER}.aab"
APK_OUT="ipv64net_v${VER}-debug.apk"
AAB_OUT="ipv64net_v${VER}-debug.aab"

cp "$APK_PATH" "$APK_OUT"
cp "$AAB_PATH" "$AAB_OUT"
Expand All @@ -83,21 +64,96 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ipv64net-${{ steps.rename.outputs.version }}
name: ipv64net-${{ steps.rename.outputs.version }}-debug
path: |
${{ steps.rename.outputs.apk }}
${{ steps.rename.outputs.aab }}

- name: Decode keystore
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > releaseKey.jks

- name: Build release APK
run: ./gradlew assembleRelease

- name: Build release AAB
run: ./gradlew bundleRelease

- name: Sign APK
run: |
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore releaseKey.jks -storepass ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
-keypass ${{ secrets.ANDROID_KEY_PASSWORD }} \
app/build/outputs/apk/release/app-release-unsigned.apk ${{ secrets.ANDROID_KEY_ALIAS }}

- name: Verify signature
run: jarsigner -verify -verbose -certs app/build/outputs/apk/release/app-release-unsigned.apk

- name: Align APK
run: |
$ANDROID_HOME/build-tools/34.0.0/zipalign -v 4 \
app/build/outputs/apk/release/app-release-unsigned.apk \
app/build/outputs/apk/release/app-release.apk

# ======= AAB signieren (falls NICHT schon durch Gradle gesigned) =======
# Wenn du signingConfigs.release in Gradle mit releaseKey.jks verwendest,
# ist das AAB bereits signiert und du kannst diesen Schritt weg lassen.
- name: Sign AAB (optional)
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore releaseKey.jks -storepass ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
-keypass ${{ secrets.ANDROID_KEY_PASSWORD }} \
app/build/outputs/bundle/release/app-release.aab ${{ secrets.ANDROID_KEY_ALIAS }}

- name: Verify AAB signature
run: jarsigner -verify -verbose -certs app/build/outputs/bundle/release/app-release.aab

# ---- Artefakte umbenennen ----
- name: Rename outputs to the correct version
id: renamesign
run: |
set -e
VER="${{ steps.ver.outputs.version_name }}"

APK_SRC="app/build/outputs/apk/release"
AAB_SRC="app/build/outputs/bundle/release"

APK_PATH=$(ls "$APK_SRC"/*release.apk | head -n1)
AAB_PATH=$(ls "$AAB_SRC"/*release.aab | head -n1)

APK_OUT="ipv64net_v${VER}-signed.apk"
AAB_OUT="ipv64net_v${VER}-signed.aab"

cp "$APK_PATH" "$APK_OUT"
cp "$AAB_PATH" "$AAB_OUT"

echo "apk=$APK_OUT" >> $GITHUB_OUTPUT
echo "aab=$AAB_OUT" >> $GITHUB_OUTPUT
echo "version=$VER" >> $GITHUB_OUTPUT

- name: Upload Signed APK & AAB
uses: actions/upload-artifact@v4
with:
name: ipv64net-${{ steps.renamesign.outputs.version }}-signed
path: |
${{ steps.renamesign.outputs.apk }}
${{ steps.renamesign.outputs.aab }}

# ---- GitHub Release erzeugen + Dateien anhängen ----
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.rename.outputs.version }}
name: "ipv64net v${{ steps.rename.outputs.version }}"
draft: false
tag_name: v${{ steps.renamesign.outputs.version }}
name: "ipv64net v${{ steps.renamesign.outputs.version }}"
draft: true
prerelease: false
files: |
${{ steps.rename.outputs.apk }}
${{ steps.rename.outputs.aab }}
${{ steps.renamesign.outputs.apk }}
${{ steps.renamesign.outputs.aab }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "de.rpicloud.ipv64net"
minSdk = 28
targetSdk = 36
versionCode = 20
versionName = "2.0.0"
versionCode = 21
versionName = "2.0.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/de/rpicloud/ipv64net/helper/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ fun Date.formatGermanTime(): String {
return format.format(this)
}

fun Date.formatDbTime(): String {
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.GERMANY)
return format.format(this)
}

fun String.parseDbDate(): String {
val dbFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
val date: Date? = runCatching { dbFormat.parse(this) }.getOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PreferencesManager {

companion object {
@SuppressLint("UseKtx")
inline fun <reified T> saveList(ctx: Context, key: String, list: List<T>) {
inline fun <reified T> saveList(ctx: Context, key: String, list: MutableList<T>) {
val gson = Gson()
val jsonText = gson.toJson(list)
with(ctx.getSharedPreferences(key, Context.MODE_PRIVATE).edit()) {
Expand Down Expand Up @@ -58,11 +58,11 @@ class PreferencesManager {
}
}

inline fun <reified T> loadList(ctx: Context, key: String): List<T> {
inline fun <reified T> loadList(ctx: Context, key: String): MutableList<T> {
val gson = Gson()
val preferences = ctx.getSharedPreferences(key, Context.MODE_PRIVATE)
val jsonText = preferences.getString(key, "[]")
val type = object : TypeToken<List<T>>() {}.type
val type = object : TypeToken<MutableList<T>>() {}.type
return gson.fromJson(jsonText, type)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ import androidx.navigation.compose.rememberNavController
import de.rpicloud.ipv64net.R
import de.rpicloud.ipv64net.helper.BiometricPromptManager
import de.rpicloud.ipv64net.helper.PreferencesManager
import de.rpicloud.ipv64net.main.startup.views.LoginView
import de.rpicloud.ipv64net.main.views.AboutView
import de.rpicloud.ipv64net.main.views.AccountDetailView
import de.rpicloud.ipv64net.main.views.AccountEditView
import de.rpicloud.ipv64net.main.views.AccountView
import de.rpicloud.ipv64net.main.views.DomainDetailView
import de.rpicloud.ipv64net.main.views.DomainDnsNewView
Expand All @@ -56,6 +59,7 @@ import de.rpicloud.ipv64net.main.views.SettingsView
import de.rpicloud.ipv64net.models.Tab
import de.rpicloud.ipv64net.models.Tabs
import de.rpicloud.ipv64net.models.Tabs.Companion.AddItem
import de.rpicloud.ipv64net.models.User
import de.rpicloud.ipv64net.ui.theme.AppTheme

class MainActivity : AppCompatActivity() {
Expand All @@ -66,17 +70,24 @@ class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
User.init(applicationContext)
enableEdgeToEdge()
setContent {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

AppTheme {

val isBiometric: Boolean = PreferencesManager.loadBool(applicationContext, "LOCKSCREEN_ENABLED")
val biometricResult by promptManager.promptResults.collectAsState(initial = null)
val enrollLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { println("Activity result $it") }

if (!isBiometric || biometricResult == BiometricPromptManager.BiometricResult.AuthenticationSuccess) {
Scaffold(bottomBar = { TabView(Tabs.tabList, navController) }) { mainPadding ->
Scaffold(bottomBar = {
if (currentRoute != Tabs.Companion.getRoute(Tab.login))
TabView(Tabs.tabList, navController)
}) { mainPadding ->
NavHost(
navController = navController,
startDestination = Tabs.Companion.getRoute(Tab.domains)
Expand Down Expand Up @@ -117,6 +128,12 @@ class MainActivity : AppCompatActivity() {
composable(Tabs.Companion.getRoute(Tab.account)) {
AccountView(navController, mainPadding = mainPadding)
}
composable(Tabs.Companion.getRoute(Tab.account_details)) {
AccountDetailView(navController, mainPadding = mainPadding)
}
composable(Tabs.Companion.getRoute(Tab.account_edit)) {
AccountEditView(navController, mainPadding = mainPadding)
}
composable(Tabs.Companion.getRoute(Tab.logs)) {
LogView(navController, mainPadding = mainPadding)
}
Expand All @@ -126,6 +143,9 @@ class MainActivity : AppCompatActivity() {
composable(Tabs.Companion.getRoute(Tab.about)) {
AboutView(navController, mainPadding = mainPadding)
}
composable(Tabs.Companion.getRoute(Tab.login)) {
LoginView(navController, true)
}
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import de.rpicloud.ipv64net.models.User
import de.rpicloud.ipv64net.ui.theme.AppTheme

class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
User.init(applicationContext)
setContent {
val navController = rememberNavController()
AppTheme {
Expand Down
Loading
Loading