From c0e58125ddae47affc08f07c7d53d426d53a5a1a Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Wed, 8 Nov 2023 21:43:03 -0500 Subject: [PATCH] add: build and release CI Add build and release pipeline for app Fixes bug where location permission screen was not appearing on < Android 9 Bump versions --- .github/workflows/android.yml | 98 +++++++++++++++ .gitignore | 2 + app/build.gradle.kts | 39 +++++- app/signing_template.properties | 0 .../ui/screens/settings/SettingsScreen.kt | 118 ++++++++++-------- gradle/libs.versions.toml | 20 +-- 6 files changed, 215 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/android.yml create mode 100644 app/signing_template.properties diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..9fd8e4f --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,98 @@ +# name of the workflow +name: Android CI Tag Deployment + +on: + push: + tags: + - '*.*.*' + +jobs: + build: + name: Build Signed APK + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Here we need to decode keystore.jks from base64 string and place it + # in the folder specified in the release signing configuration + - name: Decode Keystore + id: decode_keystore + uses: timheuer/base64-to-file@v1.2 + with: + fileName: 'android_keystore.jks' + fileDir: '/home/runner/work/wgtunnel/wgtunnel/app/keystore/' + encodedString: ${{ secrets.KEYSTORE }} + + # Build and sign APK ("-x test" argument is used to skip tests) + # add fdroid flavor for apk upload + - name: Build Fdroid Release APK + run: ./gradlew :app:assembleFdroidRelease -x test + env: + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} + + # get fdroid flavor release apk path + - name: Get apk path + id: apk-path + run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT + + # add general flavor for google play + - name: Build General Release AAB + id: buildRelease + run: ./gradlew bundleGeneralRelease + env: + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} + + # Save the APK after the Build job is complete to publish it as a Github release in the next job + - name: Upload APK + uses: actions/upload-artifact@v3.1.2 + with: + name: wgtunnel + path: ${{ steps.apk-path.outputs.path }} + + - name: Create service_account.json + id: createServiceAccount + run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json + + # upload general flavor release aab to beta track + - name: Publish to Play Store BETA + id: publish + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJson: service_account.json + packageName: com.zaneschepke.wireguardautotunnel + releaseFile: app/build/outputs/bundle/generalRelease/app-general-release.aab + track: beta + + release: + name: Release APK + needs: build + runs-on: ubuntu-latest + steps: + - name: Download APK from build + uses: actions/download-artifact@v1 + with: + name: wgtunnel + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + draft: false + prerelease: false + files: wgtunnel/${{ steps.apk-path.outputs.path }} diff --git a/.gitignore b/.gitignore index 31caa0c..c2d1e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ lint/tmp/ # App Specific cases app/release/output.json .idea/codeStyles/ +# where we keep our signing secrets locally +app/signing.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24b7e80..a288032 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -15,7 +17,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 32000 - versionName = "3.2.0" + versionName = "3.2.1" ksp { arg("room.schemaLocation", "$projectDir/schemas") @@ -29,7 +31,39 @@ android { } } + signingConfigs { + create("release") { + val properties = Properties().apply { + //created local file for signing details + try { + load(file("signing.properties").reader()) + } catch (_ : Exception) { + load(file("signing_template.properties").reader()) + } + } + val storePassVarName = "SIGNING_STORE_PASSWORD"; + val keyAliasVarName = "SIGNING_KEY_ALIAS" + val keyPassVarName = "SIGNING_KEY_PASSWORD" + val keyStorePathVarName = "KEY_STORE_PATH" + + //try to get secrets from env first for pipeline build, then properties file for local build + storeFile = file(System.getenv().getOrDefault(keyStorePathVarName, properties.getProperty(keyStorePathVarName))) + storePassword = System.getenv().getOrDefault(storePassVarName, properties.getProperty(storePassVarName)) + keyAlias = System.getenv().getOrDefault(keyAliasVarName, properties.getProperty(keyAliasVarName)) + keyPassword = System.getenv().getOrDefault(keyPassVarName, properties.getProperty(keyPassVarName)) + } + } + buildTypes { + applicationVariants.all { + val variant = this + variant.outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + .forEach { output -> + val outputFileName = "wgtunnel-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk" + output.outputFileName = outputFileName + } + } release { isDebuggable = false isMinifyEnabled = true @@ -38,6 +72,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } debug { isDebuggable = true @@ -61,6 +96,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { jvmTarget = "17" @@ -105,6 +141,7 @@ dependencies { //wg implementation(libs.tunnel) + coreLibraryDesugaring(libs.desugar.jdk.libs) //logging implementation(libs.timber) diff --git a/app/signing_template.properties b/app/signing_template.properties new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 259d9c8..56b7665 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -99,7 +99,7 @@ fun SettingsScreen( val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) var currentText by remember { mutableStateOf("") } val scrollState = rememberScrollState() - var didShowLocationDisclaimer by remember { mutableStateOf(false) } + var isLocationDisclaimerNeeded by remember { mutableStateOf(true) } var isBackgroundLocationGranted by remember { mutableStateOf(true) } var showAuthPrompt by remember { mutableStateOf(false) } var didExportFiles by remember { mutableStateOf(false) } @@ -141,6 +141,7 @@ fun SettingsScreen( return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded()) } + fun openSettings() { scope.launch { val intentSettings = @@ -156,62 +157,75 @@ fun SettingsScreen( rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) if(!backgroundLocationState.status.isGranted) { isBackgroundLocationGranted = false - if(!didShowLocationDisclaimer) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(padding) - ) { - Icon( - Icons.Rounded.LocationOff, - contentDescription = stringResource(id = R.string.map), - modifier = Modifier - .padding(30.dp) - .size(128.dp) - ) - Text( - stringResource(R.string.prominent_background_location_title), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 20.sp - ) - Text( - stringResource(R.string.prominent_background_location_message), - textAlign = TextAlign.Center, - modifier = Modifier.padding(30.dp), - fontSize = 15.sp - ) - Row( - modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier - .fillMaxWidth() - .padding(10.dp) else Modifier - .fillMaxWidth() - .padding(30.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - TextButton(onClick = { - didShowLocationDisclaimer = true - }) { - Text(stringResource(id = R.string.no_thanks)) - } - TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { - openSettings() - }) { - Text(stringResource(id = R.string.turn_on)) - } - } - } - return - } } else { + isLocationDisclaimerNeeded = false isBackgroundLocationGranted = true } } + if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if(!fineLocationState.status.isGranted) { + isBackgroundLocationGranted = false + } else { + isLocationDisclaimerNeeded = false + isBackgroundLocationGranted = true + } + } + + if(isLocationDisclaimerNeeded) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(padding) + ) { + Icon( + Icons.Rounded.LocationOff, + contentDescription = stringResource(id = R.string.map), + modifier = Modifier + .padding(30.dp) + .size(128.dp) + ) + Text( + stringResource(R.string.prominent_background_location_title), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 20.sp + ) + Text( + stringResource(R.string.prominent_background_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(30.dp), + fontSize = 15.sp + ) + Row( + modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier + .fillMaxWidth() + .padding(10.dp) else Modifier + .fillMaxWidth() + .padding(30.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + TextButton(onClick = { + isLocationDisclaimerNeeded = false + }) { + Text(stringResource(id = R.string.no_thanks)) + } + TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = { + openSettings() + }) { + Text(stringResource(id = R.string.turn_on)) + } + } + } + return + } + + + if(showAuthPrompt) { AuthorizationPrompt(onSuccess = { showAuthPrompt = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e829263..1cba935 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,23 @@ [versions] -accompanist = "0.31.2-alpha" +accompanist = "0.32.0" activityCompose = "1.8.0" androidx-junit = "1.1.5" appcompat = "1.6.1" biometricKtx = "1.2.0-alpha05" coreGoogleShortcuts = "1.1.0" coreKtx = "1.12.0" +desugar_jdk_libs = "2.0.4" espressoCore = "3.5.1" firebase-crashlytics-gradle = "2.9.9" google-services = "4.4.0" -hiltAndroid = "2.48" -hiltNavigationCompose = "1.0.0" +hiltAndroid = "2.48.1" +hiltNavigationCompose = "1.1.0" junit = "4.13.2" -kotlinx-serialization-json = "1.5.1" +kotlinx-serialization-json = "1.6.0" lifecycle-runtime-compose = "2.6.2" material-icons-extended = "1.5.4" material3 = "1.1.2" -navigationCompose = "2.7.4" +navigationCompose = "2.7.5" roomVersion = "2.6.0" timber = "5.0.1" tunnel = "1.0.20230706" @@ -24,13 +25,13 @@ androidGradlePlugin = "8.3.0-alpha06" kotlin="1.9.10" ksp="1.9.10-1.0.13" composeBom="2023.10.01" -firebaseBom="32.4.0" +firebaseBom= "32.5.0" compose="1.5.4" -crashlytics="18.5.0" -analytics="21.4.0" +crashlytics= "18.5.1" +analytics="21.5.0" composeCompiler="1.5.3" zxingAndroidEmbedded = "4.3.0" -zxingCore = "3.4.1" +zxingCore = "3.5.2" [libraries] @@ -61,6 +62,7 @@ androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-p androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" } #hilt +desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }