feat: UI compact layout + unlock support + ResticBinary init at startup

This commit is contained in:
RainySY
2026-06-07 13:37:21 +08:00
parent 5faedd53af
commit 7c780b30c0
11 changed files with 230 additions and 58 deletions

View File

@@ -13,6 +13,8 @@ import androidx.viewpager2.widget.ViewPager2
import com.example.androidbackupgui.databinding.ActivityMainBinding import com.example.androidbackupgui.databinding.ActivityMainBinding
import com.example.androidbackupgui.root.RootShell import com.example.androidbackupgui.root.RootShell
import com.example.androidbackupgui.backup.LogUtil import com.example.androidbackupgui.backup.LogUtil
import com.example.androidbackupgui.backup.ResticBinary
import com.example.androidbackupgui.backup.ResticWrapper
import com.example.androidbackupgui.ui.BackupFragment import com.example.androidbackupgui.ui.BackupFragment
import com.example.androidbackupgui.ui.ConfigFragment import com.example.androidbackupgui.ui.ConfigFragment
import com.example.androidbackupgui.ui.RestoreFragment import com.example.androidbackupgui.ui.RestoreFragment
@@ -39,6 +41,9 @@ class MainActivity : AppCompatActivity() {
// Configure libsu with global mount namespace support // Configure libsu with global mount namespace support
RootShell.configure() RootShell.configure()
// Initialize restic binary path
ResticBinary.prepare(this)?.let { ResticWrapper.binaryPath = it }
// Request root access on startup // Request root access on startup
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@@ -61,6 +61,36 @@ class ResticMaintenance(
} }
} }
// ── Unlock ──────────────────────────────────────────
suspend fun unlock(
repoPath: String,
password: String,
backend: String = "local",
backendUrl: String = "",
backendUser: String = "",
backendPass: String = "",
backendShare: String = "",
): AppResult<String> =
withContext(Dispatchers.IO) {
if (backend == "local") {
val env = envResolver.buildLocalEnv(repoPath, password, cacheDir)
val result = runner.runRestic(env, "unlock")
if (result.exitCode == 0) AppResult.Success(result.stdout)
else err(AppError.Restic("restic unlock 失败", result.exitCode, result.stderr))
} else {
bridgeRunner.withBridge(
backend, backendUrl, backendUser, backendPass, backendShare,
backendDomain, repoPath, File(cacheDir)
) { bridgeUrl, authToken ->
val env = envResolver.buildBridgeEnv(password, bridgeUrl, cacheDir, authToken)
val result = runner.runRestic(env, "unlock")
if (result.exitCode == 0) AppResult.Success(result.stdout)
else err(AppError.Restic("restic unlock 失败", result.exitCode, result.stderr))
}
}
}
// ── Check ────────────────────────────────────────── // ── Check ──────────────────────────────────────────
suspend fun check( suspend fun check(

View File

@@ -359,6 +359,20 @@ object ResticWrapper {
backend, backendUrl, backendUser, backendPass, backendShare backend, backendUrl, backendUser, backendPass, backendShare
) )
suspend fun unlock(
repoPath: String,
password: String,
backend: String = "local",
backendUrl: String = "",
backendUser: String = "",
backendPass: String = "",
backendShare: String = "",
): AppResult<String> =
maintenance.unlock(
repoPath, password,
backend, backendUrl, backendUser, backendPass, backendShare,
)
// ── Public URL helper ────────────────────────────── // ── Public URL helper ──────────────────────────────
/** Build a display-friendly repository URL for UI. */ /** Build a display-friendly repository URL for UI. */

View File

@@ -97,8 +97,8 @@ class BackupFragment : Fragment() {
val names = userList.map { (id, name) -> "$name (ID: $id)" } val names = userList.map { (id, name) -> "$name (ID: $id)" }
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names) val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.userSelector.adapter = adapter binding.backupUserSelector.adapter = adapter
binding.userSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.backupUserSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedUserId = userList.getOrNull(position)?.first ?: 0 selectedUserId = userList.getOrNull(position)?.first ?: 0
} }
@@ -401,7 +401,7 @@ class BackupFragment : Fragment() {
private fun updateOutputPathDisplay() { private fun updateOutputPathDisplay() {
val path = config.outputPath.ifEmpty { requireContext().filesDir.absolutePath } val path = config.outputPath.ifEmpty { requireContext().filesDir.absolutePath }
binding.outputPathLabel.text = path binding.outputPathLabel.text = "目录: $path"
} }

View File

@@ -67,6 +67,9 @@ class ConfigFragment : Fragment() {
binding.initResticButton.setOnClickListener { initResticRepo() } binding.initResticButton.setOnClickListener { initResticRepo() }
binding.resticStatsButton.setOnClickListener { showResticStats() } binding.resticStatsButton.setOnClickListener { showResticStats() }
binding.resticPruneButton.setOnClickListener { pruneResticSnapshots() } binding.resticPruneButton.setOnClickListener { pruneResticSnapshots() }
binding.resticUnlockButton.setOnClickListener {
vm.unlockResticRepo(readResticForm())
}
// Initial async status check // Initial async status check
refreshResticStatus() refreshResticStatus()
@@ -140,6 +143,8 @@ class ConfigFragment : Fragment() {
binding.resticStatsButton.visibility = if (statsButtonVisible) View.VISIBLE else View.GONE binding.resticStatsButton.visibility = if (statsButtonVisible) View.VISIBLE else View.GONE
binding.resticPruneButton.isEnabled = pruneButtonEnabled binding.resticPruneButton.isEnabled = pruneButtonEnabled
binding.resticPruneButton.visibility = if (pruneButtonVisible) View.VISIBLE else View.GONE binding.resticPruneButton.visibility = if (pruneButtonVisible) View.VISIBLE else View.GONE
binding.resticUnlockButton.isEnabled = unlockButtonEnabled
binding.resticUnlockButton.visibility = if (unlockButtonVisible) View.VISIBLE else View.GONE
} }
} }

View File

@@ -44,7 +44,9 @@ data class ResticStatus(
val statsButtonVisible: Boolean = false, val statsButtonVisible: Boolean = false,
val statsButtonEnabled: Boolean = true, val statsButtonEnabled: Boolean = true,
val pruneButtonVisible: Boolean = false, val pruneButtonVisible: Boolean = false,
val pruneButtonEnabled: Boolean = true val pruneButtonEnabled: Boolean = true,
val unlockButtonVisible: Boolean = false,
val unlockButtonEnabled: Boolean = true
) )
/** Restic credential/form snapshot passed from Fragment on every user interaction. */ /** Restic credential/form snapshot passed from Fragment on every user interaction. */
@@ -245,16 +247,39 @@ class ConfigViewModel(application: Application) : AndroidViewModel(application)
_uiState.update { it.copy(resticStatus = ResticStatus( _uiState.update { it.copy(resticStatus = ResticStatus(
message = "仓库就绪,${snapshots.size} 个快照", message = "仓库就绪,${snapshots.size} 个快照",
snapshotCount = snapshots.size, snapshotCount = snapshots.size,
initButtonVisible = false, statsButtonVisible = true, pruneButtonVisible = true initButtonVisible = false, statsButtonVisible = true, pruneButtonVisible = true,
unlockButtonVisible = true
))} ))}
} else { } else {
val errMsg = snapshotsResult.errorOrNull()?.message ?: ""
val hasLock = errMsg.contains("lock", ignoreCase = true) || errMsg.contains("already locked", ignoreCase = true)
_uiState.update { it.copy(resticStatus = ResticStatus( _uiState.update { it.copy(resticStatus = ResticStatus(
message = "仓库未初始化或认证失败", message = if (hasLock) "仓库被锁定,请先解锁" else "仓库未初始化或认证失败",
initButtonVisible = true, statsButtonVisible = false, pruneButtonVisible = false initButtonVisible = !hasLock, statsButtonVisible = false, pruneButtonVisible = false,
unlockButtonVisible = hasLock
))} ))}
} }
} }
} }
fun unlockResticRepo(form: ResticForm) {
_uiState.update { it.copy(resticStatus = it.resticStatus.copy(
message = "正在解锁仓库…", unlockButtonEnabled = false
))}
viewModelScope.launch {
ResticWrapper.backendDomain = form.backendDomain
val result = ResticWrapper.unlock(form.repo, form.password,
backend = form.backend, backendUrl = form.backendUrl,
backendUser = form.backendUser, backendPass = form.backendPass,
backendShare = form.backendShare,
)
_uiState.update { it.copy(resticStatus = it.resticStatus.copy(
message = if (result.isSuccess) "解锁完成" else "解锁失败: ${result.errorOrNull()?.message}",
unlockButtonEnabled = true
))}
refreshResticStatus(form)
}
}
fun showResticStats(form: ResticForm) { fun showResticStats(form: ResticForm) {
_uiState.update { it.copy(resticStatus = it.resticStatus.copy( _uiState.update { it.copy(resticStatus = it.resticStatus.copy(
message = "正在读取统计…", statsButtonEnabled = false message = "正在读取统计…", statsButtonEnabled = false
@@ -303,6 +328,15 @@ class ConfigViewModel(application: Application) : AndroidViewModel(application)
viewModelScope.launch { viewModelScope.launch {
try { try {
_operationEvents.emit(OperationEvent.PruneStarted) _operationEvents.emit(OperationEvent.PruneStarted)
// Remove stale locks before forget/prune
ResticWrapper.backendDomain = form.backendDomain
ResticWrapper.unlock(form.repo, form.password,
backend = form.backend, backendUrl = form.backendUrl,
backendUser = form.backendUser, backendPass = form.backendPass,
backendShare = form.backendShare,
)
val forgetResult = ResticWrapper.forget(form.repo, form.password, val forgetResult = ResticWrapper.forget(form.repo, form.password,
keepDaily = 7, keepWeekly = 4, keepMonthly = 3, keepDaily = 7, keepWeekly = 4, keepMonthly = 3,
backend = form.backend, backendUrl = form.backendUrl, backend = form.backend, backendUrl = form.backendUrl,

View File

@@ -83,8 +83,8 @@ class RestoreFragment : Fragment() {
val names = userList.map { (id, name) -> "$name (ID: $id)" } val names = userList.map { (id, name) -> "$name (ID: $id)" }
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names) val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.userSelector.adapter = adapter binding.restoreUserSelector.adapter = adapter
binding.userSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.restoreUserSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedUserId = userList.getOrNull(position)?.first ?: 0 selectedUserId = userList.getOrNull(position)?.first ?: 0
} }

View File

@@ -7,6 +7,7 @@
android:padding="@dimen/fragment_horizontal_padding" android:padding="@dimen/fragment_horizontal_padding"
android:background="?attr/colorSurface"> android:background="?attr/colorSurface">
<!-- Title + user selector -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -18,38 +19,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="应用备份" android:text="应用备份"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="?attr/colorOnSurface" /> android:textColor="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/scanButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="扫描应用"
style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="用户: " android:text="用户: "
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" /> android:textColor="?attr/colorOnSurfaceVariant" />
<Spinner <Spinner
android:id="@+id/userSelector" android:id="@+id/backupUserSelector"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" /> android:minWidth="80dp"
android:spinnerMode="dropdown" />
<com.google.android.material.button.MaterialButton
android:id="@+id/scanButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="扫描"
style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout> </LinearLayout>
<!-- Output path + sort toolbar -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -57,13 +53,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="输出目录: "
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:id="@+id/outputPathLabel" android:id="@+id/outputPathLabel"
android:layout_width="0dp" android:layout_width="0dp"
@@ -71,7 +60,7 @@
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="middle" android:ellipsize="middle"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" /> android:textColor="?attr/colorOnSurfaceVariant" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
@@ -80,9 +69,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:text="修改" /> android:text="修改"
android:textSize="12sp" />
</LinearLayout> </LinearLayout>
<!-- Sort/filter row -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -133,10 +124,11 @@
style="@style/Widget.Material3.Button.TonalButton" /> style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout> </LinearLayout>
<!-- System apps toggle + status -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="2dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
@@ -144,46 +136,49 @@
android:id="@+id/showSystemSwitch" android:id="@+id/showSystemSwitch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="显示系统应用" android:text="系统应用"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:checked="false" /> android:checked="false" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:text="点击扫描"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="?attr/colorPrimary" app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceVariant" /> app:trackColor="?attr/colorSurfaceVariant" />
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="3"
android:ellipsize="end"
android:text="点击扫描以载入应用列表"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/appList" android:id="@+id/appList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginTop="12dp" android:layout_marginTop="4dp"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="8dp" /> android:paddingBottom="4dp" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/backupButton" android:id="@+id/backupButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="8dp"
android:enabled="false" android:enabled="false"
android:text="开始备份选中应用" android:text="开始备份选中应用"
style="@style/Widget.Material3.Button" /> style="@style/Widget.Material3.Button" />

View File

@@ -173,7 +173,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:singleSelection="true" app:singleSelection="true"
app:selectionRequired="true"> app:selectionRequired="true"
app:checkedButton="@id/resticBackendLocal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/resticBackendLocal" android:id="@+id/resticBackendLocal"
@@ -267,6 +268,7 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:hint="密码" android:hint="密码"
android:visibility="gone" android:visibility="gone"
app:endIconMode="password_toggle"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/resticBackendPassEdit" android:id="@+id/resticBackendPassEdit"
@@ -308,10 +310,12 @@
<!-- Repo encryption password (always shown) --> <!-- Repo encryption password (always shown) -->
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/resticPasswordLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:hint="仓库加密密码 (请妥善保管,遗失即无法恢复)" android:hint="仓库加密密码 (请妥善保管,遗失即无法恢复)"
app:endIconMode="password_toggle"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/resticPasswordEdit" android:id="@+id/resticPasswordEdit"
@@ -360,6 +364,14 @@
android:text="清理" android:text="清理"
android:visibility="gone" android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton" /> style="@style/Widget.Material3.Button.TextButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/resticUnlockButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="解锁"
android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout> </LinearLayout>
<TextView <TextView

View File

@@ -53,7 +53,7 @@
android:textColor="?attr/colorOnSurfaceVariant" /> android:textColor="?attr/colorOnSurfaceVariant" />
<Spinner <Spinner
android:id="@+id/userSelector" android:id="@+id/restoreUserSelector"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" /> android:layout_weight="1" />

77
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,77 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %OS%==Windows_NT setlocal
:omega