From 446ffa4a4da6102ecb8234e877e053c96179e3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 31 Oct 2023 13:44:42 +0800 Subject: [PATCH] Replace appcenter with google services --- .gitignore | 3 +- app/build.gradle | 26 ++- app/google-services.json | 29 +++ app/src/main/AndroidManifest.xml | 9 +- .../nekohasekai/sfa/constant/SettingsKey.kt | 2 +- .../io/nekohasekai/sfa/database/Settings.kt | 8 +- .../io/nekohasekai/sfa/ui/MainActivity.kt | 171 +++++++----------- .../sfa/ui/main/SettingsFragment.kt | 44 ++--- .../ui/profileoverride/PerAppProxyActivity.kt | 2 +- .../main/play/release-notes/en-US/beta.txt | 1 + app/src/main/res/layout/fragment_settings.xml | 53 +++--- app/src/main/res/values/strings.xml | 13 +- build.gradle | 4 + gradle/wrapper/gradle-wrapper.properties | 4 +- 14 files changed, 182 insertions(+), 187 deletions(-) create mode 100644 app/google-services.json create mode 100644 app/src/main/play/release-notes/en-US/beta.txt diff --git a/.gitignore b/.gitignore index 6ff1684..8951cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ .externalNativeBuild .cxx local.properties -/app/libs/ \ No newline at end of file +/app/libs/ +/service-account-credentials.json \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ee79e2c..3e1061d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,6 +5,9 @@ plugins { id 'kotlin-android' id 'kotlin-parcelize' id 'com.google.devtools.ksp' + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' + id 'com.github.triplet.play' } android { @@ -39,13 +42,15 @@ android { if (getProps("KEYSTORE_PASS") != "") { signingConfig signingConfigs.release } - buildConfigField("String", "APPCENTER_SECRET", "\"" + getProps("APPCENTER_SECRET") + "\"") } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release - buildConfigField("String", "APPCENTER_SECRET", "\"" + getProps("APPCENTER_SECRET") + "\"") + firebaseCrashlytics { + nativeSymbolUploadEnabled true + unstrippedNativeLibsDir true + } } } @@ -96,14 +101,14 @@ dependencies { implementation 'com.blacksquircle.ui:editorkit:2.2.0' implementation 'com.blacksquircle.ui:language-json:2.2.0' - implementation 'com.microsoft.appcenter:appcenter-analytics:5.0.2' - implementation 'com.microsoft.appcenter:appcenter-crashes:5.0.2' - implementation 'com.microsoft.appcenter:appcenter-distribute:5.0.2' - implementation('org.smali:dexlib2:2.5.2') { exclude group: 'com.google.guava', module: 'guava' } implementation('com.google.guava:guava:32.1.2-android') + + implementation platform('com.google.firebase:firebase-bom:32.4.0') + implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation 'com.google.android.play:app-update-ktx:2.1.0' } if (getProps("APPCENTER_TOKEN") != "") { @@ -122,6 +127,15 @@ if (getProps("APPCENTER_TOKEN") != "") { } } +def playCredentialsJSON = rootProject.file("service-account-credentials.json") +if (playCredentialsJSON.exists()) { + play { + serviceAccountCredentials = playCredentialsJSON + defaultToAppBundles = true + track = 'beta' + } +} + tasks.withType(KotlinCompile.class).configureEach { kotlinOptions { jvmTarget = "1.8" diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..474a84b --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "548801210715", + "project_id": "sing-b0x", + "storage_bucket": "sing-b0x.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:548801210715:android:2c3bce07700eecb54d527e", + "android_client_info": { + "package_name": "io.nekohasekai.sfa" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDzb5nuF2qyw-AW0opn4Ymi2QGuJ6dZyYo" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 14ca0b9..91b99a7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,10 @@ android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> + + @@ -173,11 +177,6 @@ android:resource="@xml/cache_paths" /> - - - \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt index 40e32d9..7d21044 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt @@ -4,7 +4,7 @@ object SettingsKey { const val SELECTED_PROFILE = "selected_profile" const val SERVICE_MODE = "service_mode" - const val ANALYTICS_ALLOWED = "analytics_allowed" + const val ERROR_REPORTING_ENABLED = "error_reporting_enabled" const val CHECK_UPDATE_ENABLED = "check_update_enabled" const val DISABLE_MEMORY_LIMIT = "disable_memory_limit" diff --git a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt index 2f7b1a0..fbd9f03 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt @@ -38,11 +38,11 @@ object Settings { var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL } var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER) - const val ANALYSIS_UNKNOWN = -1 - const val ANALYSIS_ALLOWED = 0 - const val ANALYSIS_DISALLOWED = 1 + const val ERROR_REPORTING_UNKNOWN = -1 + const val ERROR_REPORTING_ALLOWED = 0 + const val ERROR_REPORTING_DISALLOWED = 1 - var analyticsAllowed by dataStore.int(SettingsKey.ANALYTICS_ALLOWED) { ANALYSIS_UNKNOWN } + var errorReportingEnabled by dataStore.int(SettingsKey.ERROR_REPORTING_ENABLED) { ERROR_REPORTING_UNKNOWN } var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { true } var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT) diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt index 3b01c50..2d6e87d 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt @@ -2,12 +2,11 @@ package io.nekohasekai.sfa.ui import android.Manifest import android.annotation.SuppressLint -import android.app.Activity import android.content.Context import android.content.Intent import android.net.VpnService import android.os.Bundle -import android.text.TextUtils +import android.util.Log import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat @@ -18,18 +17,16 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.microsoft.appcenter.AppCenter -import com.microsoft.appcenter.analytics.Analytics -import com.microsoft.appcenter.crashes.Crashes -import com.microsoft.appcenter.distribute.Distribute -import com.microsoft.appcenter.distribute.DistributeListener -import com.microsoft.appcenter.distribute.ReleaseDetails -import com.microsoft.appcenter.distribute.UpdateAction -import com.microsoft.appcenter.utils.AppNameHelper +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.InstallStatus +import com.google.android.play.core.install.model.UpdateAvailability +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.ProfileContent import io.nekohasekai.sfa.Application -import io.nekohasekai.sfa.BuildConfig import io.nekohasekai.sfa.R import io.nekohasekai.sfa.bg.ServiceConnection import io.nekohasekai.sfa.bg.ServiceNotification @@ -45,17 +42,16 @@ import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ui.profile.NewProfileActivity import io.nekohasekai.sfa.ui.shared.AbstractActivity import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.util.Date import java.util.LinkedList -class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeListener { +class MainActivity : AbstractActivity(), ServiceConnection.Callback { companion object { - private const val TAG = "MyActivity" + private const val TAG = "MainActivity" } private lateinit var binding: ActivityMainBinding @@ -86,7 +82,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL binding.navView.setupWithNavController(navController) reconnect() - startAnalysis() + startIntegration() } override fun onNewIntent(intent: Intent) { @@ -182,109 +178,76 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL connection.reconnect() } - private fun startAnalysis() { + private fun startIntegration() { lifecycleScope.launch(Dispatchers.IO) { - when (Settings.analyticsAllowed) { - Settings.ANALYSIS_UNKNOWN -> { - withContext(Dispatchers.Main) { - showAnalysisDialog() - } - } - - Settings.ANALYSIS_ALLOWED -> { - startAnalysisInternal() + if (Settings.errorReportingEnabled == Settings.ERROR_REPORTING_UNKNOWN) { + withContext(Dispatchers.Main) { + confirmErrorReportingIntegration() } + } else if (Settings.checkUpdateEnabled) { + checkUpdate() } } } - private fun showAnalysisDialog() { - val builder = MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.analytics_title)) - .setMessage(getString(R.string.analytics_message)) - .setPositiveButton(android.R.string.ok) { _, _ -> - lifecycleScope.launch(Dispatchers.IO) { - Settings.analyticsAllowed = Settings.ANALYSIS_ALLOWED - startAnalysisInternal() + private fun checkUpdate() { + val appUpdateManager = AppUpdateManagerFactory.create(this) + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + when (appUpdateInfo.updateAvailability()) { + UpdateAvailability.UPDATE_NOT_AVAILABLE -> { + Log.d(TAG, "checkUpdate: not available") + } + + UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> { + when (appUpdateInfo.installStatus()) { + InstallStatus.DOWNLOADED -> { + appUpdateManager.completeUpdate() + } + } + } + + UpdateAvailability.UPDATE_AVAILABLE -> { + if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { + appUpdateManager.startUpdateFlow( + appUpdateInfo, + this, + AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build() + ) + } else if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { + appUpdateManager.startUpdateFlow( + appUpdateInfo, + this, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() + ) + } + } + + UpdateAvailability.UNKNOWN -> { } } - .setNegativeButton(getString(R.string.no_thanks)) { _, _ -> + } + appUpdateInfoTask.addOnFailureListener { + Log.e(TAG, "checkUpdate: ", it) + } + } + + private fun confirmErrorReportingIntegration() { + val builder = MaterialAlertDialogBuilder(this).setTitle(getString(R.string.error_reporting)) + .setMessage(R.string.error_reporting_message) + .setPositiveButton(getString(R.string.ok)) { _, _ -> lifecycleScope.launch(Dispatchers.IO) { - Settings.analyticsAllowed = Settings.ANALYSIS_DISALLOWED + Settings.errorReportingEnabled = Settings.ERROR_REPORTING_ALLOWED + Firebase.crashlytics.setCrashlyticsCollectionEnabled(true) + } + }.setNegativeButton(getString(R.string.no_thanks)) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + Settings.errorReportingEnabled = Settings.ERROR_REPORTING_DISALLOWED } } runCatching { builder.show() } } - suspend fun startAnalysisInternal() { - if (BuildConfig.APPCENTER_SECRET.isBlank()) { - return - } - Distribute.setListener(this) - runCatching { - AppCenter.start( - application, - BuildConfig.APPCENTER_SECRET, - Analytics::class.java, - Crashes::class.java, - Distribute::class.java, - ) - if (!Settings.checkUpdateEnabled) { - Distribute.disableAutomaticCheckForUpdate() - } - }.onFailure { - withContext(Dispatchers.Main) { - errorDialogBuilder(it).show() - } - } - } - - override fun onReleaseAvailable(activity: Activity, releaseDetails: ReleaseDetails): Boolean { - lifecycleScope.launch(Dispatchers.Main) { - delay(2000L) - runCatching { - onReleaseAvailable0(releaseDetails) - } - } - return true - } - - private fun onReleaseAvailable0(releaseDetails: ReleaseDetails) { - val builder = MaterialAlertDialogBuilder(this) - .setTitle(getString(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_title)) - var message = if (releaseDetails.isMandatoryUpdate) { - getString(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_message_mandatory) - } else { - getString(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_message_optional) - } - message = String.format( - message, - AppNameHelper.getAppName(this), - releaseDetails.shortVersion, - releaseDetails.version - ) - builder.setMessage(message) - builder.setPositiveButton(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_download) { _, _ -> - startActivity(Intent(Intent.ACTION_VIEW, releaseDetails.downloadUrl)) - } - builder.setCancelable(false) - if (!releaseDetails.isMandatoryUpdate) { - builder.setNegativeButton(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_postpone) { _, _ -> - Distribute.notifyUpdateAction(UpdateAction.POSTPONE) - } - } - if (!TextUtils.isEmpty(releaseDetails.releaseNotes) && releaseDetails.releaseNotesUrl != null) { - builder.setNeutralButton(com.microsoft.appcenter.distribute.R.string.appcenter_distribute_update_dialog_view_release_notes) { _, _ -> - startActivity(Intent(Intent.ACTION_VIEW, releaseDetails.releaseNotesUrl)) - } - } - builder.show() - } - - override fun onNoReleaseAvailable(activity: Activity) { - } - - @SuppressLint("NewApi") fun startService() { if (!ServiceNotification.checkPermission()) { diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt index f1921d9..8159c20 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt @@ -11,8 +11,8 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isGone import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope -import com.microsoft.appcenter.AppCenter -import com.microsoft.appcenter.distribute.Distribute +import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.ktx.Firebase import io.nekohasekai.libbox.Libbox import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.R @@ -60,31 +60,18 @@ class SettingsFragment : Fragment() { reloadSettings() } } - binding.appCenterEnabled.addTextChangedListener { - lifecycleScope.launch(Dispatchers.IO) { - val allowed = EnabledType.valueOf(it).boolValue - Settings.analyticsAllowed = - if (allowed) Settings.ANALYSIS_ALLOWED else Settings.ANALYSIS_DISALLOWED - withContext(Dispatchers.Main) { - binding.checkUpdateEnabled.isEnabled = allowed - } - if (!allowed) { - AppCenter.setEnabled(false) - } else { - if (!AppCenter.isConfigured()) { - activity.startAnalysisInternal() - } - AppCenter.setEnabled(true) - } - } - } binding.checkUpdateEnabled.addTextChangedListener { lifecycleScope.launch(Dispatchers.IO) { val newValue = EnabledType.valueOf(it).boolValue Settings.checkUpdateEnabled = newValue - if (!newValue) { - Distribute.disableAutomaticCheckForUpdate() - } + } + } + binding.errorReportingEnabled.addTextChangedListener { + lifecycleScope.launch(Dispatchers.IO) { + val newValue = EnabledType.valueOf(it).boolValue + Settings.errorReportingEnabled = + if (newValue) Settings.ERROR_REPORTING_ALLOWED else Settings.ERROR_REPORTING_DISALLOWED + Firebase.crashlytics.setCrashlyticsCollectionEnabled(newValue) } } binding.disableMemoryLimit.addTextChangedListener { @@ -128,23 +115,22 @@ class SettingsFragment : Fragment() { (activity.getExternalFilesDir(null) ?: activity.filesDir) .walkTopDown().filter { it.isFile }.map { it.length() }.sum() ) - val appCenterEnabled = Settings.analyticsAllowed == Settings.ANALYSIS_ALLOWED + val errorReportingEnabled = Settings.errorReportingEnabled == Settings.ERROR_REPORTING_ALLOWED val checkUpdateEnabled = Settings.checkUpdateEnabled - val removeBackgroudPermissionPage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val removeBackgroundPermissionPage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Application.powerManager.isIgnoringBatteryOptimizations(Application.application.packageName) } else { true } withContext(Dispatchers.Main) { binding.dataSizeText.text = dataSize - binding.appCenterEnabled.text = EnabledType.from(appCenterEnabled).name - binding.appCenterEnabled.setSimpleItems(R.array.enabled) - binding.checkUpdateEnabled.isEnabled = appCenterEnabled + binding.errorReportingEnabled.text = EnabledType.from(errorReportingEnabled).name + binding.errorReportingEnabled.setSimpleItems(R.array.enabled) binding.checkUpdateEnabled.text = EnabledType.from(checkUpdateEnabled).name binding.checkUpdateEnabled.setSimpleItems(R.array.enabled) binding.disableMemoryLimit.text = EnabledType.from(!Settings.disableMemoryLimit).name binding.disableMemoryLimit.setSimpleItems(R.array.enabled) - binding.backgroundPermissionCard.isGone = removeBackgroudPermissionPage + binding.backgroundPermissionCard.isGone = removeBackgroundPermissionPage } } diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt index 4103798..bd8b8c8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt @@ -282,7 +282,7 @@ class PerAppProxyActivity : AbstractActivity() { if (scanResult.isEmpty()) { MaterialAlertDialogBuilder(this@PerAppProxyActivity) - .setTitle(R.string.message) + .setTitle(R.string.title_scan_result) .setMessage(R.string.message_scan_app_no_apps_found) .setPositiveButton(android.R.string.ok, null) .show() diff --git a/app/src/main/play/release-notes/en-US/beta.txt b/app/src/main/play/release-notes/en-US/beta.txt new file mode 100644 index 0000000..0663726 --- /dev/null +++ b/app/src/main/play/release-notes/en-US/beta.txt @@ -0,0 +1 @@ +Fixes and improvements \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 114dab5..359b1ed 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -30,17 +30,17 @@ - + android:layout_marginTop="8dp" + android:hint="@string/check_update"> + android:hint="@string/error_reporting"> - - - - - + android:layout_marginTop="8dp"> - + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 332702c..f3b1b72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,8 @@ sing-box Stop + Ok + No, thanks Dashboard Profiles @@ -80,15 +82,10 @@ Version Core Data Size - Analytics - Would you like to give SFA permission to collect analytics, send crash reports, and check update through AppCenter? - No, thanks Check Update - App Center - Feedback - Message - Send - Send Feedback + Error Reporting + Would you like to allow sing to send error reports to developers via Firebase Crashlytics? + App Settings About Android client for sing-box, the universal proxy platform. Documentation diff --git a/build.gradle b/build.gradle index 17971be..4a4020f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ buildscript { dependencies { classpath "gradle.plugin.com.betomorrow.gradle:appcenter-plugin:2.0.4" + // https://github.com/invertase/react-native-firebase/issues/6983 + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' } } @@ -10,5 +12,7 @@ plugins { id 'com.android.library' version '8.1.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false id 'com.google.devtools.ksp' version '1.9.0-1.0.12' apply false + id 'com.google.gms.google-services' version '4.4.0' apply false + id 'com.github.triplet.play' version '3.8.4' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 79b86cd..c8b7bec 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ -#Sun Aug 20 19:43:30 CST 2023 +#Mon Oct 30 15:48:15 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists